跳至主要内容

Proxy-Wasm

Proxy-Wasm is a binary interface (ABI) between a proxy (like Envoy or MOSN) and a WebAssembly virtual machine. WasmEdge can be seamlessly integrated as the high-performance runtime backend for Proxy-Wasm hosts.

This tutorial demonstrates how to build a standalone Proxy-Wasm host using the proxy-wasm-cpp-host library, write a WebAssembly filter, and execute it natively using WasmEdge.

Prerequisites

Ensure your system has the following installed:

  1. WasmEdge: Version 0.14.1 or higher. (See Install WasmEdge)
  2. Build Tools: A C++ compiler (clang/gcc), cmake, and ninja.
  3. WASI-SDK: Required to compile the WebAssembly plugin (v32 or newer).

(macOS Example: Installs WASI-SDK-32 in the Working directory and exports the WASI_SDK_PATH):

curl -fL -o wasi-sdk.tar.gz https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-32/wasi-sdk-32.0-arm64-macos.tar.gz && tar -zxf wasi-sdk.tar.gz && export WASI_SDK_PATH="$(pwd)/wasi-sdk-32.0-arm64-macos" && echo $WASI_SDK_PATH
  1. System Libraries: OpenSSL and Abseil (Google's C++ library).

(macOS Example):

brew install cmake ninja abseil openssl

1. Fetch Dependencies

Create a working directory and fetch the required Proxy-Wasm upstream dependencies:

mkdir -p proxy-wasm-demo/.deps
cd proxy-wasm-demo

# Clone SDK and Host libraries
git clone --depth=1 https://github.com/proxy-wasm/proxy-wasm-cpp-sdk .deps/proxy-wasm-cpp-sdk
git clone --depth=1 https://github.com/proxy-wasm/proxy-wasm-cpp-host .deps/proxy-wasm-cpp-host

2. Write the WebAssembly Plugin

Create a directory named plugin. This filter hooks into the HTTP response phase, logs the request, and injects a custom x-powered-by: WasmEdge header.

Create plugin/plugin.cc:

// plugin/plugin.cc
//
// proxy-wasm filter using the raw ABI headers (no protobuf dependency).
// Uses the exact types defined in proxy_wasm_enums.h / proxy_wasm_common.h.
//
// What it does:
// - Logs on every HTTP request.
// - Adds "x-powered-by: WasmEdge" to every HTTP response.

#include <cstdint>
#include <cstring>
#include <string_view>

#include "proxy_wasm_common.h" // WasmResult, WasmHeaderMapType
#include "proxy_wasm_enums.h" // LogLevel, FilterHeadersStatus, FilterDataStatus
#include "proxy_wasm_externs.h" // proxy_log, proxy_add_header_map_value, …

static void log_info(std::string_view msg) {
proxy_log(LogLevel::info, msg.data(), msg.size());
}

extern "C" {

__attribute__((export_name("proxy_abi_version_0_2_0"))) void
proxy_abi_version_0_2_0() {}

__attribute__((export_name("proxy_on_vm_start"))) uint32_t
proxy_on_vm_start(uint32_t, uint32_t) {
log_info("[AddHeaderFilter] VM started on WasmEdge runtime.");
return 1;
}

__attribute__((export_name("proxy_on_configure"))) uint32_t
proxy_on_configure(uint32_t, uint32_t) {
log_info("[AddHeaderFilter] Plugin configured.");
return 1;
}

__attribute__((export_name("proxy_on_context_create"))) void
proxy_on_context_create(uint32_t, uint32_t) {}

__attribute__((export_name("proxy_on_request_headers"))) FilterHeadersStatus
proxy_on_request_headers(uint32_t, uint32_t, uint32_t) {
log_info("[AddHeaderFilter] Incoming HTTP request — passing through.");
return FilterHeadersStatus::Continue;
}

__attribute__((export_name("proxy_on_response_headers"))) FilterHeadersStatus
proxy_on_response_headers(uint32_t, uint32_t, uint32_t) {
constexpr std::string_view key = "x-powered-by";
constexpr std::string_view value = "WasmEdge";
proxy_add_header_map_value(
WasmHeaderMapType::ResponseHeaders, // correct enum name
key.data(), key.size(), value.data(), value.size());
log_info(
"[AddHeaderFilter] Injected 'x-powered-by: WasmEdge' into response.");
return FilterHeadersStatus::Continue;
}

__attribute__((export_name("proxy_on_request_body"))) FilterDataStatus
proxy_on_request_body(uint32_t, uint32_t, uint32_t) {
return FilterDataStatus::Continue;
}

__attribute__((export_name("proxy_on_response_body"))) FilterDataStatus
proxy_on_response_body(uint32_t, uint32_t, uint32_t) {
return FilterDataStatus::Continue;
}

__attribute__((export_name("proxy_on_done"))) uint32_t proxy_on_done(uint32_t) {
return 1;
}

__attribute__((export_name("proxy_on_log"))) void proxy_on_log(uint32_t) {}

__attribute__((export_name("proxy_on_delete"))) void proxy_on_delete(uint32_t) {
}

} // extern "C"

Create plugin/CMakeLists.txt:

cmake_minimum_required(VERSION 3.14)
project(proxy_wasm_add_header_plugin CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(NOT DEFINED PROXY_WASM_CPP_SDK_PATH)
message(FATAL_ERROR "Set -DPROXY_WASM_CPP_SDK_PATH=<path>")
endif()

# ── wasi-sdk 25+ (Clang 22) fix ───────────────────────────────────────────────
# Newer wasi-sdk releases place C++ stdlib headers under a target-specific
# subdirectory of the sysroot (wasm32-wasip1/c++/v1/) rather than a flat
# include path. Derive the SDK prefix from the toolchain file and wire up:
# 1. --sysroot pointing to share/wasi-sysroot
# 2. The target-specific C++ include directory
if(DEFINED CMAKE_TOOLCHAIN_FILE)
# Toolchain file lives at <wasi-sdk>/share/cmake/wasi-sdk-p1.cmake
get_filename_component(_WASI_CMAKE_DIR "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY)
get_filename_component(WASI_SDK_PREFIX "${_WASI_CMAKE_DIR}/../.." ABSOLUTE)
set(WASI_SYSROOT "${WASI_SDK_PREFIX}/share/wasi-sysroot")
if(EXISTS "${WASI_SYSROOT}")
set(WASI_CXX_INCLUDE "${WASI_SYSROOT}/include/wasm32-wasip1/c++/v1")
add_compile_options(--sysroot=${WASI_SYSROOT})
if(EXISTS "${WASI_CXX_INCLUDE}")
include_directories(SYSTEM "${WASI_CXX_INCLUDE}")
message(STATUS "wasi-sdk sysroot : ${WASI_SYSROOT}")
message(STATUS "wasi-sdk C++ hdrs : ${WASI_CXX_INCLUDE}")
endif()
endif()
endif()
# ──────────────────────────────────────────────────────────────────────────────

add_executable(plugin plugin.cc)
target_include_directories(plugin PRIVATE "${PROXY_WASM_CPP_SDK_PATH}")

set_target_properties(plugin PROPERTIES
OUTPUT_NAME "add_header_filter"
SUFFIX ".wasm"
# -mexec-model=reactor : builds a WASI reactor instead of command (no main() required)
# --export-all : export all our ABI callbacks to the host
# --export=malloc : explicitly export malloc from wasi-libc since proxy-wasm relies on it
# --export=free : explicitly export free
# --allow-undefined : host imports resolved at runtime
LINK_FLAGS "-mexec-model=reactor -Wl,--export-all -Wl,--export=malloc -Wl,--export=free -Wl,--allow-undefined"
)

Compile the WebAssembly plugin using the WASI-SDK:

# 2. Configure using the p1 toolchain
cmake -S plugin -B build/plugin \
-DCMAKE_TOOLCHAIN_FILE=$WASI_SDK_PATH/share/cmake/wasi-sdk-p1.cmake \
-DPROXY_WASM_CPP_SDK_PATH=$(pwd)/.deps/proxy-wasm-cpp-sdk \
-DCMAKE_BUILD_TYPE=Release -G Ninja

# 3. Build!
cmake --build build/plugin

3. Write the Native Host

Create a directory named host. The host initializes a WasmEdge virtual machine, loads the compiled WebAssembly filter, and simulates HTTP network traffic.

Create host/main.cc:

// host/main.cc
//
// Minimal proxy-wasm host that demonstrates WasmEdge as the runtime backend.
//
// It:
// 1. Creates a WasmEdge VM through proxy-wasm-cpp-host's factory.
// 2. Loads the compiled add_header_filter.wasm plugin.
// 3. Simulates an HTTP exchange (request headers → response headers).
// 4. Prints the resulting response headers so you can see "x-powered-by:
// WasmEdge".

#include <chrono>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>

// proxy-wasm-cpp-host public headers
#include "include/proxy-wasm/context.h"
#include "include/proxy-wasm/wasm.h"

// WasmEdge runtime factory (added by PR #193)
#include "include/proxy-wasm/wasmedge.h"

// ────────────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────────────

namespace {

using HeaderMap = std::vector<std::pair<std::string, std::string>>;

std::string readFile(const std::string &path) {
std::ifstream f(path, std::ios::binary);
if (!f) {
std::cerr << "ERROR: Cannot open '" << path << "'\n";
std::exit(1);
}
std::ostringstream buf;
buf << f.rdbuf();
return buf.str();
}

void printHeaders(const std::string &label, const HeaderMap &hdrs) {
std::cout << "\n── " << label << " ──\n";
for (auto &[k, v] : hdrs) {
std::cout << " " << k << ": " << v << "\n";
}
}

// ── DemoContext
// ───────────────────────────────────────────────────────────────
//
// A concrete ContextBase that:
// • stores simulated request and response header maps
// • implements the subset of HeaderInterface actually called by a typical
// HTTP filter (add/get/getSize/getPairs)
// • prints plugin log lines to stdout

class DemoContext : public proxy_wasm::ContextBase {
public:
DemoContext(proxy_wasm::WasmBase *wasm,
const std::shared_ptr<proxy_wasm::PluginBase> &plugin)
: proxy_wasm::ContextBase(wasm, plugin) {}

// ── HeaderInterface overrides ─────────────────────────────────────────────

proxy_wasm::WasmResult addHeaderMapValue(proxy_wasm::WasmHeaderMapType type,
std::string_view key,
std::string_view value) override {
headerMap(type).emplace_back(key, value);
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult getHeaderMapValue(proxy_wasm::WasmHeaderMapType type,
std::string_view key,
std::string_view *result) override {
for (auto &[k, v] : headerMap(type)) {
if (k == key) {
*result = v;
return proxy_wasm::WasmResult::Ok;
}
}
*result = {};
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult getHeaderMapPairs(proxy_wasm::WasmHeaderMapType type,
proxy_wasm::Pairs *result) override {
auto &map = headerMap(type);
result->clear();
for (auto &[k, v] : map)
result->emplace_back(k, v);
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult
setHeaderMapPairs(proxy_wasm::WasmHeaderMapType type,
const proxy_wasm::Pairs &pairs) override {
auto &map = headerMap(type);
map.clear();
for (auto &[k, v] : pairs)
map.emplace_back(k, v);
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult
removeHeaderMapValue(proxy_wasm::WasmHeaderMapType type,
std::string_view key) override {
auto &map = headerMap(type);
map.erase(std::remove_if(map.begin(), map.end(),
[&](const auto &p) { return p.first == key; }),
map.end());
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult
replaceHeaderMapValue(proxy_wasm::WasmHeaderMapType type,
std::string_view key, std::string_view value) override {
for (auto &[k, v] : headerMap(type)) {
if (k == key) {
v = value;
return proxy_wasm::WasmResult::Ok;
}
}
headerMap(type).emplace_back(key, value);
return proxy_wasm::WasmResult::Ok;
}

proxy_wasm::WasmResult getHeaderMapSize(proxy_wasm::WasmHeaderMapType type,
uint32_t *result) override {
*result = static_cast<uint32_t>(headerMap(type).size());
return proxy_wasm::WasmResult::Ok;
}

// ── GeneralInterface partial overrides ───────────────────────────────────

proxy_wasm::WasmResult log(uint32_t level,
std::string_view message) override {
const char *lvl = "INFO";
if (level == 0)
lvl = "TRACE";
else if (level == 1)
lvl = "DEBUG";
else if (level == 3)
lvl = "WARN";
else if (level == 4)
lvl = "ERROR";
std::cout << "[" << lvl << "] " << message << "\n";
return proxy_wasm::WasmResult::Ok;
}

uint64_t getCurrentTimeNanoseconds() override {
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count());
}

// ── Header storage (public for the driver) ────────────────────────────────
HeaderMap request_headers;
HeaderMap response_headers;

private:
HeaderMap &headerMap(proxy_wasm::WasmHeaderMapType type) {
if (type == proxy_wasm::WasmHeaderMapType::RequestHeaders)
return request_headers;
return response_headers;
}
};

// ── DemoWasmVmIntegration
// ──────────────────────────────────────────────────────
class DemoWasmVmIntegration : public proxy_wasm::WasmVmIntegration {
public:
DemoWasmVmIntegration *clone() override {
return new DemoWasmVmIntegration();
}
proxy_wasm::LogLevel getLogLevel() override {
return proxy_wasm::LogLevel::trace;
}
void error(std::string_view message) override {
std::cerr << "[WasmVm Error] " << message << std::endl;
}
void trace(std::string_view message) override {
std::cout << "[WasmVm Trace] " << message << std::endl;
}
bool getNullVmFunction(std::string_view /*function_name*/,
bool /*returns_word*/, int /*number_of_arguments*/,
proxy_wasm::NullPlugin * /*plugin*/,
void * /*ptr_to_function_return*/) override {
return false;
}
};

// ── DemoWasm ─────────────────────────────────────────────────────────────────
//
// Extends WasmBase only to supply our DemoContext factory and VM integration.

class DemoWasm : public proxy_wasm::WasmBase {
public:
DemoWasm(std::unique_ptr<proxy_wasm::WasmVm> vm, std::string_view vm_id)
: proxy_wasm::WasmBase(std::move(vm), vm_id,
/*vm_configuration=*/"",
/*vm_key=*/"",
/*envs=*/{},
/*allowed_capabilities=*/{}) {
wasm_vm()->integration().reset(new DemoWasmVmIntegration());
}

proxy_wasm::ContextBase *createRootContext(
const std::shared_ptr<proxy_wasm::PluginBase> &plugin) override {
auto *ctx = new DemoContext(this, plugin);
demo_ctx_ = ctx;
return ctx;
}

proxy_wasm::ContextBase *createContext(
const std::shared_ptr<proxy_wasm::PluginBase> &plugin) override {
return new DemoContext(this, plugin);
}

DemoContext *demo_ctx() const { return demo_ctx_; }

private:
DemoContext *demo_ctx_{nullptr};
};

// ── DemoWasmHandle
// ────────────────────────────────────────────────────────────

class DemoWasmHandle : public proxy_wasm::WasmHandleBase {
public:
explicit DemoWasmHandle(std::shared_ptr<proxy_wasm::WasmBase> wasm)
: proxy_wasm::WasmHandleBase(std::move(wasm)) {}
};

} // anonymous namespace

// ────────────────────────────────────────────────────────────────────────────
// main
// ────────────────────────────────────────────────────────────────────────────

int main(int argc, char *argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <path/to/add_header_filter.wasm>\n";
return 1;
}
const std::string wasm_path = argv[1];

std::cout << "==============================================\n";
std::cout << " proxy-wasm + WasmEdge demo\n";
std::cout << "==============================================\n\n";

// 1. Read the compiled Wasm plugin from disk.
std::string wasm_bytes = readFile(wasm_path);
std::cout << "Loaded plugin: " << wasm_path << " (" << wasm_bytes.size()
<< " bytes)\n";

// 2. Construct our DemoWasm host instance backed by WasmEdge.
const std::string vm_id = "demo_vm";
auto demo_wasm =
std::make_shared<DemoWasm>(proxy_wasm::createWasmEdgeVm(), vm_id);
std::cout << "Runtime: WasmEdge ("
<< demo_wasm->wasm_vm()->getEngineName() << ")\n";

// 3. Load and initialise the Wasm module.
if (!demo_wasm->load(wasm_bytes, /*allow_precompiled=*/false)) {
std::cerr << "ERROR: Failed to load Wasm module.\n";
return 1;
}
if (!demo_wasm->initialize()) {
std::cerr << "ERROR: Failed to initialize Wasm module.\n";
return 1;
}

// 4. Create a PluginBase that carries metadata the Wasm plugin can query.
const std::string root_id = "add_header_filter";
auto plugin = std::make_shared<proxy_wasm::PluginBase>(
/*name=*/"add_header_filter",
/*root_id=*/root_id,
/*vm_id=*/vm_id,
/*engine=*/"wasmedge",
/*plugin_configuration=*/"",
/*fail_open=*/false,
/*key=*/"");

// 5. Start the root context (calls the plugin's proxy_on_vm_start /
// proxy_on_configure).
auto *root_ctx = demo_wasm->start(plugin);
if (!root_ctx) {
std::cerr << "ERROR: Failed to start root context.\n";
return 1;
}
demo_wasm->configure(root_ctx, plugin);
std::cout << "Root context created (root_id=\"" << root_id << "\")\n";

// ── Simulate HTTP request / response ────────────────────────────────────

// Grab the root context so we can seed the header maps.
auto *ctx = demo_wasm->demo_ctx();
ctx->request_headers = {
{":method", "GET"},
{":path", "/api/hello"},
{":authority", "example.com"},
{"user-agent", "curl/7.88.1"},
{"accept", "*/*"},
};
ctx->response_headers = {
{":status", "200"},
{"content-type", "application/json"},
{"content-length", "27"},
};

printHeaders("Request headers (before filter)", ctx->request_headers);
printHeaders("Response headers (before filter)", ctx->response_headers);

// 6. Run the HTTP filter lifecycle on the root context.
// (For proper stream contexts use WasmBase::createContext + callOnThread;
// running directly on the root context is sufficient for this demo.)
std::cout << "\n── Running onRequestHeaders ──\n";
root_ctx->onRequestHeaders(static_cast<uint32_t>(ctx->request_headers.size()),
/*end_of_stream=*/false);

std::cout << "\n── Running onResponseHeaders ──\n";
root_ctx->onResponseHeaders(
static_cast<uint32_t>(ctx->response_headers.size()),
/*end_of_stream=*/false);

printHeaders("Response headers (after filter)", ctx->response_headers);

std::cout << "\n==============================================\n";
std::cout << " Demo complete! Check 'x-powered-by: WasmEdge' above.\n";
std::cout << "==============================================\n";

return 0;
}

Create host/CMakeLists.txt:

cmake_minimum_required(VERSION 3.14)
project(proxy_wasm_wasmedge_demo CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ── Required paths (pass via -D on the cmake command line) ────────────────────
# PROXY_WASM_CPP_HOST_PATH – root of the proxy-wasm-cpp-host clone
# WASMEDGE_INSTALL_PATH – WasmEdge install prefix (default: ~/.wasmedge)

if(NOT DEFINED PROXY_WASM_CPP_HOST_PATH)
message(FATAL_ERROR
"Set PROXY_WASM_CPP_HOST_PATH to your proxy-wasm-cpp-host source tree.\n"
" cmake -DPROXY_WASM_CPP_HOST_PATH=... .")
endif()

if(NOT DEFINED PROXY_WASM_CPP_SDK_PATH)
set(PROXY_WASM_CPP_SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../.deps/proxy-wasm-cpp-sdk")
endif()

if(NOT DEFINED WASMEDGE_INSTALL_PATH)
set(WASMEDGE_INSTALL_PATH "$ENV{HOME}/.wasmedge")
endif()

set(HOST_ROOT "${PROXY_WASM_CPP_HOST_PATH}")

# ── WasmEdge shared library ───────────────────────────────────────────────────
find_library(WASMEDGE_LIB
NAMES wasmedge
HINTS "${WASMEDGE_INSTALL_PATH}/lib"
REQUIRED)

message(STATUS "WasmEdge library : ${WASMEDGE_LIB}")

# ── OpenSSL ───────────────────────────────────────────────────────────────────
find_package(OpenSSL REQUIRED)
message(STATUS "OpenSSL include : ${OPENSSL_INCLUDE_DIR}")
message(STATUS "OpenSSL library : ${OPENSSL_LIBRARIES}")

# ── Abseil (required by proxy-wasm-cpp-host for absl::cleanup) ───────────────
if(NOT DEFINED ABSL_ROOT_DIR)
# Try common Homebrew locations first
if(EXISTS "/opt/homebrew/opt/abseil")
set(ABSL_ROOT_DIR "/opt/homebrew/opt/abseil")
elseif(EXISTS "/usr/local/opt/abseil")
set(ABSL_ROOT_DIR "/usr/local/opt/abseil")
endif()
endif()
if(DEFINED ABSL_ROOT_DIR)
list(PREPEND CMAKE_PREFIX_PATH "${ABSL_ROOT_DIR}")
endif()
find_package(absl REQUIRED)
message(STATUS "Abseil include : ${ABSL_ROOT_DIR}/include")

# ── proxy-wasm-cpp-host sources ───────────────────────────────────────────────
# The host library is built as part of the main project here so that we can
# enable the WasmEdge backend. In a real project you would add the host as a
# CMake subdirectory or consume its installed targets.

set(HOST_SOURCES
# Core base library
"${HOST_ROOT}/src/bytecode_util.cc"
"${HOST_ROOT}/src/context.cc"
"${HOST_ROOT}/src/exports.cc"
"${HOST_ROOT}/src/hash.cc"
"${HOST_ROOT}/src/pairs_util.cc"
"${HOST_ROOT}/src/shared_data.cc"
"${HOST_ROOT}/src/shared_queue.cc"
"${HOST_ROOT}/src/signature_util.cc"
"${HOST_ROOT}/src/vm_id_handle.cc"
"${HOST_ROOT}/src/wasm.cc"
# Null VM (needed for NullIntegration base class)
"${HOST_ROOT}/src/null/null.cc"
"${HOST_ROOT}/src/null/null_plugin.cc"
"${HOST_ROOT}/src/null/null_vm.cc"
# WasmEdge runtime backend (the whole point of the demo)
"${HOST_ROOT}/src/wasmedge/wasmedge.cc"
)

add_library(proxy_wasm_host STATIC ${HOST_SOURCES})

target_include_directories(proxy_wasm_host PUBLIC
"${HOST_ROOT}"
"${HOST_ROOT}/include"
"${PROXY_WASM_CPP_SDK_PATH}"
"${WASMEDGE_INSTALL_PATH}/include"
"${OPENSSL_INCLUDE_DIR}"
)
target_link_libraries(proxy_wasm_host PUBLIC
"${WASMEDGE_LIB}"
OpenSSL::SSL
OpenSSL::Crypto
absl::cleanup
absl::base
absl::strings
)
target_compile_definitions(proxy_wasm_host PUBLIC WITH_WASMEDGE=1)

# ── Demo executable ───────────────────────────────────────────────────────────
add_executable(demo main.cc)
target_link_libraries(demo PRIVATE proxy_wasm_host)
target_include_directories(demo PRIVATE
"${HOST_ROOT}"
"${HOST_ROOT}/include"
"${PROXY_WASM_CPP_SDK_PATH}"
"${WASMEDGE_INSTALL_PATH}/include"
"${OPENSSL_INCLUDE_DIR}"
)

Compile the Native Host:

cmake -S host -B build/host \
-DPROXY_WASM_CPP_HOST_PATH=$(pwd)/.deps/proxy-wasm-cpp-host \
-DWASMEDGE_INSTALL_PATH=$HOME/.wasmedge \
-DCMAKE_BUILD_TYPE=Release -G Ninja

cmake --build build/host

4. Execution

Run the compiled native host and pass it the WebAssembly plugin. Ensure the WasmEdge library is in your path.

# For macOS use DYLD_LIBRARY_PATH, for Linux use LD_LIBRARY_PATH
LD_LIBRARY_PATH=$HOME/.wasmedge/lib ./build/host/demo ./build/plugin/add_header_filter.wasm

Expected Output

Upon successful execution, the host will load the WebAssembly filter and simulate network traffic. The output will explicitly indicate that the plugin injected the custom x-powered-by: WasmEdge header:

Loaded plugin: .../add_header_filter.wasm
Runtime: WasmEdge (wasmedge)

── Request headers (before filter) ──
:method: GET
:path: /api/hello

── Response headers (before filter) ──
:status: 200

── Running onResponseHeaders ──
[INFO] [AddHeaderFilter] Injected 'x-powered-by: WasmEdge' into response.

── Response headers (after filter) ──
:status: 200
x-powered-by: WasmEdge

Congrats! You have now compiled and run a Proxy-Wasm filter natively with WasmEdge. Now you have the building blocks to develop high-performance WebAssembly plugins that can inspect, route, and securely mutate network traffic across your cloud-native infrastructure.