C++ on the Compute platform
The Compute platform supports application code written in C++, a language for high-performance applications.
Quick access
C++ language support
The C++ SDK for Compute uses the WASI C++ toolchain to build C++ applications to Wasm binaries.
In order to develop for Fastly Compute in C++, obtain the following tools for your platform:
- CMake 3.x 3.25 or newer, not compatible with CMake 4
- GNU make
- WASI SDK 25.0 or newer
- Fastly CLI 14.0 or newer
This document assumes that cmake, make, and fastly are available on the system path, and that the WASI SDK is available at /opt/wasi-sdk.
HINT: For best results, expand the SDK to /opt/ and create a symbolic link (e.g., /opt/wasi-sdk) to maintain a consistent path across version updates.
Example (macOS Apple Silicon):
sudo tar -xzf ./wasi-sdk-32.0-arm64-macos.tar.gz -C /optsudo ln -sfn /opt/wasi-sdk-32.0-arm64-macos /opt/wasi-sdk# On macOS, remove the provenance attribute to allow the compiler to run:xattr -rd com.apple.quarantine /opt/wasi-sdkFor running unit tests, also obtain Viceroy (0.15.0 or newer) and place it on the system path.
Verify your environment
Before proceeding, ensure your tools are responsive:
cmake --version # Should be 3.25 or newerhead -1 /opt/wasi-sdk/VERSION # Should be 25 or newerfastly version # Should be 14.0 or newer
# For unit testingviceroy --version # Should be 0.15.0 or newerProject layout
If you don't yet have a working toolchain and Compute service set up, start by getting set up.
A new project initialized from the default starter kit will contain a simple file tree:
├── .fastlyignore├── .gitignore├── CMakeLists.txt├── CMakePresets.json├── README.md├── fastly.toml└── src └── main.cppBuilding and running the application
To turn your source code into a running service, you first configure the build system and then start the local testing server.
Configuration
The recommended way to manage the build process is to use CMake presets. The starter kit includes a CMakePresets.json file that defines the wasi preset, which has been set up to use the WASI-SDK toolchain.
{ "version": 3, "configurePresets": [ { "name": "wasi", "generator": "Unix Makefiles", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_TOOLCHAIN_FILE": "/opt/wasi-sdk/share/cmake/wasi-sdk-p1.cmake" } } ]}Configure the project by running cmake against this preset.
cmake --preset wasiThe build-and-serve Loop
The easiest way to build and test your application is using the Fastly CLI.
fastly compute serveWhen you run the above command, the CLI looks at the [scripts.build] value in your fastly.toml file, and runs this build script automatically before starting the local server:
[scripts] build = "cmake --build build && cp build/main.wasm bin/main.wasm"Once the server is running (usually on http://127.0.0.1:7676), you can test it by accessing it via a web browser, or by using curl in a separate terminal:
curl -i http://127.0.0.1:7676/If you are running the default starter kit, this request will return a HTML response.
Key files
src/main.cpp: The entry point of your application, which contains the logic you'll run on incoming requests.CMakeLists.txt: Describes your dependencies and build steps.- If you add more source files (like
src/utils.cpp), you must add them to the add_executable() command in this file so the compiler knows to include them.
- If you add more source files (like
CMakePresets.json: Provides configuration presets (e.g.,wasifor production,wasi-testingfor testing).fastly.toml: Contains metadata required by Fastly to deploy your package. Learn more aboutfastly.toml.
The Fastly dependency
The Fastly C++ SDK is added to the application using the FetchContent module in CMakeLists.txt. This automatically downloads the SDK at configuration time:
include(FetchContent)FetchContent_Declare( fastly_sdk URL https://github.com/fastly/compute-sdk-cpp/releases/download/v0.6.0/fastly-cpp-v0.6.0.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP NEW SYSTEM)FetchContent_MakeAvailable(fastly_sdk)find_package(fastly REQUIRED PATHS ${fastly_sdk_SOURCE_DIR} NO_DEFAULT_PATH)The SYSTEM keyword enables the compiler to treat the SDK as a third-party library. Once configured, you can include the SDK in your source:
#include <fastly/sdk.h>Main interface
Start a Compute program by defining a main() function:
#include <fastly/sdk.h>
int main() { auto req = fastly::Request::from_client();
// Simple synthetic response auto res = fastly::Response::from_status(200) .with_body("Hello from the edge!");
res.send_to_client(); return 0;}The Fastly Compute C++ SDK provides the core object types such as Request, Response, and Body referenced by your application.
The program will be invoked for each request that Fastly receives for a domain attached to your service, and it is expected to send a response that can be served to the client.
The Request and Response objects
The Fastly C++ SDK uses the fastly::Request and fastly::Response classes to model HTTP traffic. These classes provide a structured way to capture, modify, and route data between your users and your backends.
Working with requests
An application typically interacts with requests in one of two ways:
- Downstream (from the client): Use
fastly::Request::from_client()to access the request sent by the user to the Fastly edge. - Synthetic: Use
fastly::Requestconstructors to manually build a new request from scratch.
Once an object is instantiated, you can inspect or manipulate it using built-in methods such as:
- Examine: Retrieve metadata using
get_headers(),get_method(), orget_url(). - Modify: Change behavior using
set_ttl(),set_pass(), or by adding headers directly to the request withreq.get_headers().insert(...).
Communicating with backend servers and the Fastly cache
A fastly::Request can be forwarded to any upstream backend defined on your service.
To retrieve content from an origin, you "send" a request to a defined backend.
constexpr auto backend_name = "my_backend_name";auto res = req.send(backend_name);HINT: It's a good idea to define backend names as constants.
Requests forwarded to a backend will typically transit the Fastly readthrough cache interface, and the response may come from cache. See Readthrough (HTTP) cache for more precise or explicit control over the Fastly readthrough cache.
Additionally, the SDK supports dynamic backends created at runtime using fastly::backend::BackendBuilder.
Handling failures
Because network operations can fail, the .send() method returns a fastly::expected container.
auto result = req.send(backend_name);
if (!result) { // Error path: Generate a local synthetic response fastly::Response{}.with_status(502).send_to_client(); return 1; }
// Success path: Send the backend response as-is result->send_to_client(); // This is equivalent to result.value().send_to_client(); IMPORTANT: This is a common pattern in this SDK. Rather than utilizing traditional C++ exceptions, return values from such operations favor a safer and more explicit check, forcing a clean branching path in your code. A fastly::expected<T> will either contain a valid T or a FastlyError object.
To keep examples concise, this documentation assumes that calls succeeded. In a production environment, you should always check the fastly::expected result to handle potential system failures gracefully.
Handling and returning responses
A response is either returned from a backend fetch or created locally (a "synthetic response") to handle redirects or error pages.
- Upstream responses: Take the successful return value from
fastly::Request::send(). - Creation: Instantiate
fastly::Response()directly to create custom content. - Inspection: Use
get_status()orget_headers()to verify the backend's data. - Downstream (send to client): Use
.send_to_client()to stream the response back to the user. This is typically the final action your application takes.
NOTE: Calling .send_to_client() begins the streaming process. Once called, you can no longer modify the response headers or status code.
Parsing and transforming responses
The Fastly Compute platform is built for high-performance streaming. Request and Response in Compute services are streams, allowing large payloads to move through your service without running out of memory.
While you can read the entire body into memory using take_body_string(), this should be reserved for small payloads (like JSON metadata).
The following example reads a backend response into memory, replaces every occurrence of "cat" with "dog" in the body, and then creates a new body with the transformed string:
auto api_resp = api_req.send("example_backend");
if (!api_resp) { fastly::Response{}.with_status(500).send_to_client(); return 1; } auto api_resp_body = api_resp->take_body();
// Take care! take_body_string() will consume the entire body into // memory, and regex_replace() will further double the memory requirement auto api_resp_body_string = api_resp_body.take_body_string(); auto new_body = std::regex_replace(api_resp_body_string, std::regex{"cat"}, "dog");
api_resp->set_body(new_body); For large streams, it is best to avoid loading the entire body into a single string or vector. Instead, use the body as a stream by calling read() in a loop to handle the contents piecewise. This allows your application to process massive payloads while maintaining a constant, minimal memory footprint.
In this example, we process a backend response 1 KiB at a time to calculate a basic checksum:
auto body = resp_result->take_body(); uint64_t total_checksum = 0; std::vector<uint8_t> buffer(1024); // Fixed 1 KiB buffer
while (true) { auto read_result = body.read(buffer.data(), buffer.size());
// Check for error or end of stream if (!read_result || read_result.value() == 0) { break; }
// Perform calculation on the current chunk size_t bytes_read = read_result.value(); total_checksum = std::accumulate(buffer.begin(), buffer.begin() + bytes_read, total_checksum);
// The buffer is reused in the next iteration, // ensuring memory usage never exceeds ~1 KiB. }
std::cout << "Processing complete. Checksum: " << total_checksum << std::endl; C++ does not have built-in support for handling data such as HTML, JSON, XML, etc. Parsing these types of responses in C++ often benefits from well-tested third-party libraries. The following section describes an example of parsing JSON using such a library.
Parsing JSON responses
For structured data, the nlohmann/json library is a popular choice that works well on the Compute platform.
To use it, add the following to your CMakeLists.txt using the same FetchContent pattern used for the SDK:
FetchContent_Declare( json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3 SYSTEM)FetchContent_MakeAvailable(json)
# Link it to your executabletarget_link_libraries(main PRIVATE nlohmann_json::nlohmann_json)HINT: Because modern C++ JSON libraries often rely on exceptions for error handling, ensure you have JSON_NOEXCEPTION defined or handle the -fno-exceptions flag as discussed in the "no-exceptions" rule section.
The following example attempts to parse a body as JSON, but limits the memory impact by only reading the first 4KiB of the stream.
auto body = resp_result->take_body();
// Limit read to 4KiB to protect memory std::vector<uint8_t> buffer(4096); auto read_result = body.read(buffer.data(), buffer.size());
if (read_result && read_result.value() > 0) { // Trim buffer to actual bytes read buffer.resize(*read_result);
// Parse JSON safely. Note: with -fno-exceptions, // nlohmann::json will call std::abort() on parse errors. auto data = json::parse(buffer, nullptr, false);
if (!data.is_discarded()) { if (data.contains("status") && data["status"] == "success") { // ... logic based on JSON content ... } } } Compression
Fastly can compress and decompress content automatically, and it is often easier to use these features than to try to perform compression or decompression within your C++ code. Learn more about compression with Fastly.
Using edge data
Fastly provides high-performance data stores for configuration and state. All edge data resources are account-level, service-linked resources, allowing a single store to be accessed from multiple Fastly services.
Accessing these stores in C++ requires handling fastly::expected (for system errors) and std::optional (for missing data). Therefore, you should use the .has_value() function and/or the ! operator to check for and handle failures gracefully.
Config store
Best for application configuration. Note that for Config Stores, the result is nested: an expected containing an optional.
auto store_res = fastly::config_store::ConfigStore::open("app_config"); // expected<ConfigStore> if (!store_res) { std::cout << "Failed to open Config Store" << std::endl; fastly::Response::from_status(500).send_to_client(); return 1; }
auto val_res = store_res->get("api_url"); // expected<optional<string>> if (val_res && val_res->has_value()) { // Access the inner string via the value() of the optional std::cout << "API URL: " << val_res->value() << std::endl; } Secret Store
Used for sensitive data. This follows a similar pattern to the Config Store but returns a Secret object that must be explicitly decrypted.
auto store_res = fastly::secret_store::SecretStore::open("my_secrets"); // expected<SecretStore> if (!store_res) { std::cout << "Failed to open Secret Store" << std::endl; fastly::Response::from_status(500).send_to_client(); return 1; }
auto secret_res = store_res->get("api_key"); // expected<optional<Secret>> if (secret_res && secret_res->has_value() ) { auto key = secret_res->value().plaintext(); // std::string // ... use key ... } KV Store
The KV Store is unique because it treats values as streams (just like HTTP bodies) and tracks versioning metadata.
When you call lookup(), you get a LookupResult which provides:
take_body(): Access the value as aBody.metadata(): Access any custom metadata associated with the key.current_generation(): A unique identifier for the specific version of the data (useful for concurrency control).
To write or update a value, call insert().
auto store_res = fastly::kv_store::KVStore::open("sessions"); // expected<optional<KVStore>> if (!store_res || !store_res->has_value()) { std::cout << "Failed to open KV Store" << std::endl; fastly::Response::from_status(500).send_to_client(); return 1; } auto kv_store = store_res->value();
// Read from KV Store auto entry_res = kv_store.lookup("user_123"); // expected<LookupResponse> if (entry_res && entry_res.has_value()) { auto body_str = entry_res->take_body().take_body_string(); auto meta = entry_res->metadata(); auto version = entry_res->current_generation();
std::cout << "Data: " << body_str << " (Gen: " << version << ")" << std::endl; }
// Write to KV Stores at runtime auto kv_store_write_result = kv_store.insert("user_123", "active"); if (kv_store_write_result) { std::cout << "Wrote to KV Store!" << std::endl; } For more advanced usages of the KV Store, such as for enumerating keys or using the builder pattern to query or update entries, see the C++ SDK documentation page on the KV Store.
Logging
Observability is your primary window into how your logic behaves at the edge. The C++ SDK provides flexible ways to output data, ranging from standard C++ streams to specialized, high-performance log endpoints.
Standard output logging (std::cout)
Any data sent to std::cout or std::cerr is captured by the Fastly runtime.
- Locally: Logs appear in your local testing server terminal.
- Production: Logs can be viewed in real-time using the
fastly log-tailcommand.
std::cout << "Processing request for: " << req.get_url() << std::endl;For additional information on logging for testing and debugging, refer to the Testing and debugging section below.
Real-time log streaming
For production environments, you typically want to send data to a named endpoint (e.g., S3, Datadog, or a custom syslog server). The SDK offers two ways to handle this:
Global logging (
init_simple)This is the easiest way to add logging across your entire application. You initialize a global logger with a specific endpoint and a severity filter.
📄 src/main.cpp - Global logging with init_simple()C++// Route all standard log calls to "my_endpoint_name"// and only send warnings or higher.fastly::log::init_simple("my_endpoint_name", fastly::log::LogLevelFilter::Warn);fastly::log::warn("This will be sent to the endpoint.");fastly::log::info("This will be ignored (below the 'Warn' threshold).");Targeted logging (
Endpoint::from_name())Use this when you need to send different types of data to different places (e.g., sending "Audit Logs" to S3 and "Performance Metrics" to Datadog).
📄 src/main.cpp - Targeted logging with fastly::Endpoint::from_name()C++auto audit_log = fastly::log::Endpoint::from_name("audit_logs");audit_log << "Sensitive security event detected." << std::endl;
Structured JSON logging
To make your logs searchable in production, send them as JSON. This allows your log provider to index specific fields like status codes, durations, or custom IDs.
#include <nlohmann/json.hpp> nlohmann::json j = { {"event", "checksum_calculated"}, {"value", checksum}, {"timestamp", std::time(nullptr)} };
auto endpoint = fastly::log::Endpoint::from_name("analytics"); endpoint << j.dump() << std::endl; Comparison of Logging Methods
| Method | Best Use Case | Logic |
|---|---|---|
std::cout | Debugging / Prototyping | Standard stream; visible in log-tail. |
fastly::log::init_simple() | General App Logging | Global filter; maps to fastly::log::info/warn/error. |
fastly::log::Endpoint::from_name() | Multi-destination | Direct control over specific named endpoints. |
Using dependencies
The Compute build process compiles your C++ code to WebAssembly using the WASI-SDK. Because of this, it supports C++ libraries that are either freestanding or specifically compatible with the WASI sysroot.
The process of adding a dependency library to a C++ application varies more than other platforms. Refer to the instructions provided with the specific library.
Integrating libraries with FetchContent
For external dependencies that support it, an efficient method is CMake's FetchContent module. This automates the download and integration of source code during the configuration step.
include(FetchContent)
FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/github/nlohmann/json.git GIT_TAG v3.11.3 # Can be a tag, branch, or commit hash SYSTEM # Suppresses warnings from dependency headers)
FetchContent_MakeAvailable(nlohmann_json)
# Link the dependency to your applicationtarget_link_libraries(your_app PRIVATE nlohmann_json::nlohmann_json)Compatibility requirements
While many modern, header-only, or freestanding C++ libraries work seamlessly, the WASI system interface has strict limitations.
| Feature Category | Likely to Work? | Notes |
|---|---|---|
| Header-only | ✅ Yes | Libraries like nlohmann/json or glm are highly compatible. |
| Algorithms/math | ✅ Yes | Pure computational logic (e.g., Eigen, FastFloat) works well. |
| Multithreading | ❌ No | Libraries requiring <pthreads.h> or std::thread will fail to link. |
| Local networking | ❌ No | Access to <sys/socket.h> is not supported in the WASI sandbox. |
| OS frameworks | ❌ No | Anything requiring a GUI (Win32, Cocoa) or local filesystems will fail. |
Our Fiddle tool allows the use of an approved subset of modules tested for compatibility with the Compute platform. As a lightweight experimentation environment, Fiddle supports core functionality from these modules, though not all features of every module are supported.
Inja(3.5.0) (In Fiddle, use#include <inja.hpp>.)ada-url(3.4.4) (In Fiddle, use#include <ada.h>.)nlohmann/json(3.12.0) (In Fiddle, use#include <nlohmann/json.hpp>.)simdutf(9.0.0) (In Fiddle, use#include <simdutf.h>.)stduuid(1.2.3) (In Fiddle, use#include <uuid.h>.)Magic Enum C++(0.9.8) (In Fiddle, use#include <magic_enum/magic_enum.hpp>.)toml++(3.4.0) (In Fiddle, use#include <toml.hpp>.)
The "no-exceptions" rule
Fastly Compute applications must be compiled with -fno-exceptions. This is a requirement for the WebAssembly environment to ensure predictable performance and smaller binary sizes.
When integrating third-party libraries, keep the following in mind:
- Library configuration: Many libraries (like
nlohmann/json) have specific macros to disable their internal exception handling (e.g.,#define JSON_NOEXCEPTION). - Stubbing missing symbols: If a library compiles but fails at the linking stage with errors like
undefined reference to __cxa_throw, it means the library is still trying to use C++ exceptions. Sometimes, stubbing them can get them to work.HINT: For example, in order to run Catch2, stub the following functions:
Stubbing missing exception symbolsC++#ifdef __wasm__extern "C" {void *__cxa_allocate_exception(size_t) { return nullptr; }void __cxa_throw(void *, void *, void *) { __builtin_trap(); }void __cxa_free_exception(void *) {}}#endif - Runtime behavior: When exceptions are disabled, most libraries will call
std::abort()or__builtin_trap()if they encounter a fatal error. Ensure your input validation is robust so these error paths are never triggered.
Verifying whether a library is compatible
To verify if a library is compatible with Fastly Compute, attempt to configure your project with the WASI toolchain: cmake --preset wasi.
- Check for missing headers: Errors pointing to
<sys/socket.h>,<pthreads.h>, or<dlfcn.h>indicate the library relies on unsupported OS features. - Watch for linker errors: If the build fails at the linking stage with symbols like
pthread_createorfork, the library is attempting to call unimplemented POSIX interfaces. - Audit preprocessor macros: Some libraries use
#ifdef _WIN32or#ifndef __wasm__to provide "no-op" stubs. Be aware that these stubs may return errors or trigger astd::terminateat runtime.
HINT: When using FetchContent, CMake automatically propagates your WASI toolchain settings to the dependency. This ensures the library is cross-compiled for WebAssembly specifically for your target environment.
Link-time optimization (LTO)
To achieve the best possible performance and the smallest binary size, the C++ SDK supports Interprocedural Optimization (IPO), also known as Link-Time Optimization (LTO).
Standard compilation optimizes each .cpp file (translation unit) in isolation. LTO allows the linker to look across all files in your project to eliminate dead code and inline functions across the entire binary. This is particularly effective for WebAssembly, where binary size directly impacts startup time.
You can enable LTO in your CMakeLists.txt with the following block:
HINT: This is included by default in the starter kit.
option(ENABLE_LTO "Enable cross language linking time optimization" ON)
if(ENABLE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT supported OUTPUT error) if(supported) message(STATUS "IPO / LTO enabled") # Tells the compiler to optimize across different translation units set_target_properties(main PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) # Force the use of the LLVM Linker (lld), which is required for Wasm LTO target_link_options(main PRIVATE -fuse-ld=lld) else() message(STATUS "IPO / LTO not supported: <${error}>") endif()endif()By using LTO, you ensure that the final Wasm module is as lean as possible, reducing the "Cold Start" time of your service when it is first invoked at the edge.
Testing and debugging
Logging to standard output (via std::cout, printf(), etc.) is the primary way to observe how your program behaves at the edge.
Log output from live services can be monitored via live log tailing. The local test server and Fastly Fiddle display all log output automatically. See Testing & debugging for more information about choosing an environment in which to test your program.
Local development server You can run your application on your local machine in a local testing server. It creates a local version of the Fastly environment, allowing you to run your Wasm binary and see exactly how it behaves before deploying.
All
std::coutandstd::cerroutput is piped directly to your terminal.fastly compute serveLive log tailing Once your service is live, you can use log tailing to monitor
std::coutandstd::cerroutput from your globally deployed application instances in real-time from your console. This is essential for debugging issues against real customer traffic, or for features that don't have support in the local testing server.# View production logs as they happenfastly log-tailFiddle If you are testing your application using the Fastly Fiddle tool, it displays
std::coutandstd::cerroutput in the Fiddle interface.
Debugging request and response bodies by "peeking"
Most common logging requirements involve inspecting HTTP requests and responses. However, since HTTP bodies are forward-only streams, they can only be consumed once. This means that simply reading a body to produce a log message will disrupt the rest of your program.
To solve this, you can use a body restoration pattern. To "peek" at a body, we read a small amount of data into a local buffer, then create a new body containing those bytes and append the original stream to it. Using a C++ Template allows this logic to work seamlessly for both Request and Response objects. With the template helper, logging request and response data becomes a clean one-liner that doesn't interfere with your send() or send_to_client() calls.
// We use a template 'T' so this works for both Request and Responsetemplate <typename T>std::string peek_and_restore(T& message, size_t len) { // 1. Take the body from the message auto body = message.take_body();
// 2. Read the prefix std::vector<uint8_t> buffer(len); auto read_res = body.read(buffer.data(), buffer.size());
if (!read_res || *read_res == 0) { message.set_body(std::move(body)); // Put it back if empty return ""; }
size_t n = *read_res; std::string prefix(reinterpret_cast<char*>(buffer.data()), n);
// 3. Restore the body by prepending the bytes we read auto restored_body = fastly::Body(); if (restored_body.write(buffer.data(), n)) { restored_body.append(std::move(body)); }
// 4. Put the restored body back into the message message.set_body(std::move(restored_body));
return prefix;} std::cout << "Request:" << std::endl; std::cout << "User-Agent: " << header_val(req.get_header("User-Agent").value()) << std::endl; std::cout << "POST Body: " << peek_and_restore(req, 1024) << std::endl; std::cout << "Response:" << std::endl; std::cout << "Content-Type: " << header_val(beresp.get_header("Content-Type").value()) << std::endl; std::cout << "Response Prefix: " << peek_and_restore(beresp, 20) << std::endl; HINT: This pattern is highly efficient. Only the "peeked" bytes are buffered in memory; the rest of the body continues to stream through the Fastly host handles.
Handling WebAssembly traps
When a C++ program performs an illegal operation (like accessing a failed fastly::expected via .value()), it "Traps."
- In the local testing server: You will see a detailed error message and potentially a stack trace in your terminal.
- In production: The visitor will receive a
500 Internal Server Error.
IMPORTANT: In a production-ready service, always check expected.has_value() before calling .value(). A trap in production is difficult to debug because it provides no custom log data before termination.
Unit testing
Because the Fastly C++ SDK relies on WebAssembly hostcalls (functions provided by the Fastly runtime), you cannot use standard native tools to test SDK-dependent code directly. If you try to run a test that would access a fastly object, the program will crash because the Host environment isn't there to answer.
One recommended strategy is to employ the Interface/Logic separation pattern. By writing your application logic to accept generic interfaces rather than concrete SDK objects, you can run tests against "Mock" classes without relying on code that invokes hostcalls.
Catch2 is a simple testing framework that is compatible with the Compute C++ SDK. This section describes how to set up a C++ application that uses Catch2 with this strategy.
Project structure
Use a standard C++ layout to keep your headers, implementation, and tests distinct:
my-service/├── include/│ ├── interfaces.h # Abstract base classes│ └── utils.h # Headers for utils.cpp├── src/│ ├── main.cpp # The Fastly entry point│ └── utils.cpp # Business logic implementation├── tests/│ ├── main.cpp # Test entry point│ └── test_utils.cpp # Catch2 test cases├── CMakeLists.txt├── CMakePresets.json└── fastly.tomlIn our somewhat-contrived example below, we will be writing and testing is_mobile(), a function that tests a request's User-Agent header to detect whether it has been sent from a mobile device.
Define the interface
Instead of passing a fastly::Request directly to your functions, we will define an interface that lets the caller get a header value. This allows you to swap a real Request for a "Mock" Request during testing.
#pragma once#include <string>#include <optional>
// An interface for any object that can provide headersclass HeaderSource {public: virtual ~HeaderSource() = default; virtual std::optional<std::string> get_header(const std::string& name) const = 0;};#pragma once#include "interfaces.h"
bool is_mobile(const HeaderSource& source);Write the logic
This is the code we want to test. The business logic remains "pure". It doesn't know about the Fastly SDK; it only knows about the HeaderSource interface.
#include "utils.h"
// Logic that determines if a request is from a mobile devicebool is_mobile(const HeaderSource& source) { auto ua = source.get_header("User-Agent"); if (!ua) return false; return ua->find("Mobile") != std::string::npos;}Write the Catch2 test cases
Using Catch2, we can structure our tests into TEST_CASE and SECTION blocks for high readability.
#include <catch2/catch_test_macros.hpp>#include "interfaces.h"#include "utils.h"
// A Mock implementation for testingclass MockSource : public HeaderSource { std::string mock_ua;public: MockSource(std::string ua) : mock_ua(ua) {} std::optional<std::string> get_header(const std::string& name) const override { if (name == "User-Agent") return mock_ua; return std::nullopt; }};
TEST_CASE("Device detection logic", "[utils]") { SECTION("Detects mobile devices") { MockSource mobile_req("Mozilla/5.0 (iPhone; CPU iPhone OS 14_0) Mobile"); CHECK(is_mobile(mobile_req) == true); }
SECTION("Detects desktop devices") { MockSource desktop_req("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); CHECK(is_mobile(desktop_req) == false); }
SECTION("Handles missing User-Agent") { MockSource empty_req(""); CHECK(is_mobile(empty_req) == false); }}The test entry point
Since we compile with -fno-exceptions, we need to define CATCH_CONFIG_RUNNER and provide a main() to run our Catch session.
#define CATCH_CONFIG_RUNNER#include <catch2/catch_all.hpp>
#ifdef __wasm__extern "C" {
// Stubs needed to get Catch2 to linkvoid *__cxa_allocate_exception(size_t) { return nullptr; }void __cxa_throw(void *, void *, void *) { __builtin_trap(); }void __cxa_free_exception(void *) {}
}#endif
int main(int argc, char* argv[]) { // Add wasi-specific setup here if needed return Catch::Session().run(argc, argv);}Build configuration
To run these Catch2 tests, we compile them into a WebAssembly binary and execute them inside Viceroy, the local testing server.
# Catch2 testing utilityFetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.4.0 SYSTEM)add_compile_definitions(CATCH_CONFIG_NO_POSIX_SIGNALS)add_compile_definitions(CATCH_CONFIG_DISABLE_EXCEPTIONS)FetchContent_MakeAvailable(Catch2)
add_executable(unit_tests tests/main.cpp tests/test_utils.cpp src/utils.cpp)target_include_directories(unit_tests PRIVATE include)
if(NOT BUILD_TESTING) # NOTE - Move this step into here target_link_libraries(main PRIVATE fastly::fastly ) # Don't build unit_tests app set_target_properties(unit_tests PROPERTIES EXCLUDE_FROM_ALL TRUE)else() # Link the unit_tests app target_link_libraries(unit_tests PRIVATE fastly::sdk Catch2::Catch2WithMain ) # Don't build the main app set_target_properties(main PROPERTIES EXCLUDE_FROM_ALL TRUE)endif(){ "name": "wasi-tests", "generator": "Unix Makefiles", "binaryDir": "${sourceDir}/build-tests", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "BUILD_TESTING": "ON", "CMAKE_TOOLCHAIN_FILE": "/opt/wasi-sdk/share/cmake/wasi-sdk-p1.cmake" }}Running the tests
Now you can build your test suite as a Wasm binary and execute it using Viceroy. Viceroy provides the necessary WebAssembly environment so the Wasm binary can execute safely.
# 1. Configure and Buildcmake --preset wasi-testscmake --build build-tests
# 2. Run the tests inside the Viceroy simulationviceroy run build-tests/wasm_tests.wasmThe following example output is produced:
Randomness seeded to: 1154335898===============================================================================All tests passed (3 assertions in 1 test case)This workflow gives you the best of both worlds: your code is architected to be modular and testable, but you are still verifying the final binary in the actual WebAssembly environment it will inhabit.
The application entry point
In your application's code, you:
- Create a concrete implementation of the interface that talks to the Fastly SDK
- Coordinate the execution by wrapping the incoming request and passing it to your business logic
// Concrete implementation of HeaderSource interacts with actual Fastly Request object.class FastlyHeaderSource : public HeaderSource { fastly::Request& _req;public: FastlyHeaderSource(fastly::Request& req) : _req(req) {}
std::optional<std::string> get_header(const std::string& name) const override { auto res = _req.get_header(name); // returns expected<optional<HeaderValue>> if (res && res->has_value() && res->value().string().has_value()) { return std::string(res->value().string().value()); } return std::nullopt; }}; // 1. Initialize the incoming request from the client auto req = fastly::Request::from_client();
// 2. Wrap the request in our interface implementation FastlyHeaderSource source(req);
// 3. Use our testable business logic! bool mobile = is_mobile(source);
// 4. Build and send the response auto resp = fastly::Response::from_status(200); resp.with_body(mobile ? "Hello, mobile user!" : "Hello, desktop user!"); resp.send_to_client(); By structuring your code this way, your main.cpp becomes a thin "adapter" layer.
- The SDK is isolated: Only this file and your
FastlyHeaderSourceknow about the Fastly SDK types. - The logic is pure: Your
utils.cppstays clean, portable, and 100% testable on any platform. - The tests are fast: You can run thousands of unit tests against your
is_mobilelogic using theMockSourcewithout ever needing to compile a Wasm binary or start Viceroy.
Key takeaways and next steps
The Fastly C++ SDK gives you the closest possible access to the metal of the edge. Now that you have the tools to build, test, and debug effectively, you're ready to push the boundaries of what's possible at the network's edge.
As you work with C++ on Fastly Compute, keep in mind the following:
- Embrace move semantics: Understand that
fastly::Bodyandfastly::Requestare move-only resources that represent live system handles. - Trust but verify: Handle the
expected<T>return values to navigate the layers of potential failure at the edge. - Architect for testability: Keep your core logic "pure" so your code is testable without relying on concrete Fastly object.