This repository contains a header-only C++20 implementation of the WebAssembly
Component Model Canonical ABI. The public API is aggregated by
include/cmcpp.hpp; tests, code-generation tools, and runtime samples are built
as separate executables around the library.
The library uses templates, concepts, constexpr metadata, and static_assert
checks to describe Component Model values at compile time. For each supported
host type, its Component Model kind, memory size, alignment, and WebAssembly
flat representation are available through ValTrait<T>. This lets invalid or
unsupported type combinations fail during compilation, while lifting, lowering,
and guest-memory access remain runtime operations.
- Ubuntu 24.04
- Ubuntu 26.04
- macOS (latest, builds and passes tests)
- Windows 2019
- Windows 2022
The C++ aliases use the canonical value kind followed by _t: for example,
canonical OptionType(T) is represented as cmcpp::option_t<T>, and
ListType(T) as cmcpp::list_t<T>.
| Type | Real-world example |
|---|---|
| Bool | cmcpp::bool_t feature_enabled |
| S8 | int8_t temperature_delta |
| U8 | uint8_t channel_id |
| S16 | int16_t altitude_delta |
| U16 | uint16_t network_port |
| S32 | int32_t file_offset |
| U32 | uint32_t message_length |
| S64 | int64_t timestamp |
| U64 | uint64_t byte_count |
| F32 | cmcpp::float32_t sensor_reading |
| F64 | cmcpp::float64_t exchange_rate |
| Char | cmcpp::char_t initial |
| Strings (UTF-8, UTF-16, Latin-1+UTF-16) | cmcpp::string_t user_name |
| List | cmcpp::list_t<cmcpp::string_t> tags |
| Fixed-length list | cmcpp::fixed_list_t<cmcpp::float32_t, 3> rgb |
| Map | cmcpp::map_t<cmcpp::string_t, uint32_t> inventory |
| Record | cmcpp::record_t<Account> account |
| Tuple | cmcpp::tuple_t<cmcpp::string_t, uint32_t> user_id_and_age |
| Variant | cmcpp::variant_t<cmcpp::string_t, int32_t> setting |
| Enum | cmcpp::enum_t<OrderStatus> status |
| Option | cmcpp::option_t<cmcpp::string_t> middle_name |
| Result | cmcpp::result_t<Order, cmcpp::string_t> response |
| Flags | cmcpp::flags_t<"read", "write", "admin"> permissions |
| Streams (readable/writable) | cmcpp::make_stream_descriptor<LogEntry>() |
| Futures (readable/writable) | cmcpp::make_future_descriptor<Response>() |
| Own | An owned ResourceType handle for a file or socket |
| Borrow | A borrowed ResourceType handle passed to a call |
Bool. Use cmcpp::bool_t for a two-state setting such as whether a feature is enabled. A host can lower feature_enabled directly and let the canonical ABI represent it as an i32 value.
cmcpp::bool_t feature_enabled = true;S8. A signed 8-bit value is useful for a small signed delta, such as int8_t temperature_delta in a thermostat message. Values stay compact while preserving negative changes.
int8_t temperature_delta = -2;U8. Use uint8_t channel_id for a small non-negative identifier such as a radio channel, protocol version, or palette index.
uint8_t channel_id = 11;S16. A signed 16-bit value fits measurements such as int16_t altitude_delta, where the value may represent a climb or descent relative to a reference point.
int16_t altitude_delta = -120;U16. Use uint16_t network_port for a TCP or UDP port number. The unsigned range also works well for bounded counters and protocol fields.
uint16_t network_port = 443;S32. File offsets and signed coordinate deltas are common int32_t values. For example, int32_t file_offset can represent a position relative to the beginning of a mapped region.
int32_t file_offset = 4096;U32. Use uint32_t message_length for a byte length, record count, or other non-negative value whose range is larger than 16 bits.
uint32_t message_length = 1024;S64. Timestamps represented as signed 64-bit values can carry time values or differences across a wide range. A host might exchange int64_t timestamp in microseconds from an agreed epoch.
int64_t timestamp = 1'725'000'000'000'000;U64. Use uint64_t byte_count for large file sizes, monotonically increasing sequence numbers, or counters that must not become negative.
uint64_t byte_count = 12'000'000'000ULL;F32. cmcpp::float32_t sensor_reading is appropriate when a sensor or graphics pipeline prioritizes compact storage and single-precision range.
cmcpp::float32_t sensor_reading = 21.5f;F64. Use cmcpp::float64_t exchange_rate for calculations where accumulated rounding error matters, such as currency conversion or geographic coordinates.
cmcpp::float64_t exchange_rate = 1.0842;Char. cmcpp::char_t initial stores a Unicode scalar value, making it suitable for a user initial, a parsed code point, or a single internationalized label character.
cmcpp::char_t initial = U'G';Strings. Use cmcpp::string_t user_name for UTF-8 text, cmcpp::u16string_t for UTF-16-oriented APIs, or cmcpp::latin1_u16string_t when the canonical encoding may be Latin-1 or UTF-16.
cmcpp::string_t user_name = "Grace";
cmcpp::u16string_t display_name = u"Grace";List. A cmcpp::list_t<cmcpp::string_t> tags models a variable-length collection such as search labels or capabilities. The list representation carries both its guest-memory pointer and its element count.
cmcpp::list_t<cmcpp::string_t> tags = {"wasm", "cpp"};Fixed-length list. cmcpp::fixed_list_t<cmcpp::float32_t, 3> rgb models exactly three color channels. The length is part of the C++ type, so a four-channel value cannot be passed accidentally where RGB is required.
cmcpp::fixed_list_t<cmcpp::float32_t, 3> rgb = {0.2f, 0.4f, 0.8f};Map. Use cmcpp::map_t<cmcpp::string_t, uint32_t> inventory for keyed data such as item names and quantities. The canonical representation treats the map as a list of key-value tuples.
cmcpp::map_t<cmcpp::string_t, uint32_t> inventory{{"books", 4}};Record. A user-defined aggregate such as struct Account { uint32_t id; cmcpp::string_t email; }; can be wrapped as cmcpp::record_t<Account>. Its fields are lowered in declaration order with canonical alignment.
struct Account {
uint32_t id;
cmcpp::string_t email;
};
cmcpp::record_t<Account> account{7, "user@example.com"};
auto flat_account = cmcpp::lower_flat(cx, account);Tuple. cmcpp::tuple_t<cmcpp::string_t, uint32_t> user_id_and_age is useful for a small unnamed pair returned by a helper. Use a record instead when the fields need stable, readable names.
using UserIdAndAge = cmcpp::tuple_t<cmcpp::string_t, uint32_t>;
UserIdAndAge user_id_and_age{"user-7", 42};
auto user_id = std::get<0>(user_id_and_age);
auto age = std::get<1>(user_id_and_age);Variant. cmcpp::variant_t<cmcpp::string_t, int32_t> setting can represent a setting supplied either as text or as a numeric value. The active alternative becomes the canonical discriminant and payload.
using Setting = cmcpp::variant_t<cmcpp::string_t, int32_t>;
Setting text_setting{cmcpp::string_t{"dark"}};
Setting numeric_setting{30};
auto flat_setting = cmcpp::lower_flat(cx, text_setting);Enum. cmcpp::enum_t<OrderStatus> status represents a closed set such as pending, shipped, and cancelled. The WIT enum supplies the labels while the C++ representation carries its numeric discriminant.
enum class OrderStatus : uint32_t { pending, shipped, cancelled };
cmcpp::enum_t<OrderStatus> status = static_cast<uint32_t>(OrderStatus::shipped);
bool is_complete = status == static_cast<uint32_t>(OrderStatus::cancelled);Option. cmcpp::option_t<cmcpp::string_t> middle_name distinguishes an absent middle name from an empty string. This corresponds to the canonical none and some cases rather than using a sentinel string.
using MiddleName = cmcpp::option_t<cmcpp::string_t>;
MiddleName present = cmcpp::string_t{"Ada"};
MiddleName absent = std::nullopt;
if (present) {
auto name = *present;
}Result. cmcpp::result_t<Order, cmcpp::string_t> response models an operation that either returns an order or an error message. The success and error alternatives remain distinct even though both travel through the same canonical result shape.
struct Order {
uint32_t id;
cmcpp::string_t status;
};
using OrderResult = cmcpp::result_t<cmcpp::record_t<Order>, cmcpp::string_t>;
OrderResult success{cmcpp::record_t<Order>{42, "shipped"}};
OrderResult failure{cmcpp::string_t{"order not found"}};Flags. cmcpp::flags_t<"read", "write", "admin"> permissions describes independent capabilities. Individual labels can be tested or changed with the flag helpers instead of manually managing a bit mask.
cmcpp::flags_t<"read", "write", "admin"> permissions;
permissions.set<"read">();
permissions.set<"write">();
if (permissions.test<"read">() && !permissions.test<"admin">()) {
// Read and write are allowed; administration is not.
}Streams. cmcpp::make_stream_descriptor<LogEntry>() describes a stream of log entries. Readable and writable ends can be created with the canonical stream operations, joined to a waitable set, and copied incrementally through guest memory.
auto log_stream = cmcpp::make_stream_descriptor<LogEntry>();
uint64_t ends = cmcpp::canon_stream_new(instance, log_stream, trap);
uint32_t readable = static_cast<uint32_t>(ends);
uint32_t writable = static_cast<uint32_t>(ends >> 32);
cmcpp::canon_stream_drop_readable(instance, readable, trap);
cmcpp::canon_stream_drop_writable(instance, writable, trap);Futures. cmcpp::make_future_descriptor<Response>() describes one eventual response. A readable end can wait for one value while a writable end completes it, with cancellation and readiness reported through the canonical future operations.
auto response_future = cmcpp::make_future_descriptor<Response>();
uint64_t ends = cmcpp::canon_future_new(instance, response_future, trap);
uint32_t readable = static_cast<uint32_t>(ends);
uint32_t writable = static_cast<uint32_t>(ends >> 32);
cmcpp::canon_future_drop_readable(instance, readable, trap);
// Complete it with canon_future_write(instance, response_future, writable, ...).
// Then drop the writable end with canon_future_drop_writable(...).Own. An owned ResourceType handle can represent a file, socket, or database connection whose destructor is controlled by the resource implementation. Dropping the handle runs the destructor after outstanding borrows and lends have ended.
cmcpp::ResourceType file_resource(instance);
uint32_t file_rep = 17;
uint32_t file_handle = cmcpp::canon_resource_new(instance, file_resource, file_rep, trap);
uint32_t same_rep = cmcpp::canon_resource_rep(instance, file_resource, file_handle, trap);
cmcpp::canon_resource_drop(instance, file_resource, file_handle, trap);Borrow. A borrowed ResourceType handle lets a call use an existing file or socket without taking ownership. The lift/lower context tracks the borrow scope and prevents the resource from being dropped while the call still uses it.
// The WIT function signature contains borrow<file-resource>.
// The owning handle stays alive for the duration of the call.
uint32_t borrowed_rep = cmcpp::canon_resource_rep(
instance, file_resource, file_handle, trap);- lower_flat_values
- lift_flat_values
- ABI
- WasmTime
- Wamr
- WasmEdge
- CMake 3.5 or higher (3.22+ recommended for presets)
- C++20 compatible compiler
- vcpkg for dependency management
- Rust toolchain with
cargo(for additional tools)
Ubuntu/Linux:
sudo apt-get install -y autoconf autoconf-archive automake build-essential ninja-build
# Optional: for creating RPM packages
sudo apt-get install -y rpmmacOS:
brew install pkg-config autoconf autoconf-archive automake coreutils libtool cmake ninjaWindows:
- Visual Studio 2019 or 2022 with C++ support
cargo install wasm-tools wit-bindgen-cliFor header-only usage without tests or samples:
git clone https://github.com/LexisNexis-GHCPE/component-model-cpp.git
cd component-model-cpp
git submodule update --init --recursive
mkdir build && cd build
cmake .. -DBUILD_TESTING=OFF -DBUILD_SAMPLES=OFF
cmake --build .Using CMake presets with vcpkg:
git clone https://github.com/LexisNexis-GHCPE/component-model-cpp.git
cd component-model-cpp
git submodule update --init --recursive
# Configure and build
cmake --preset linux-ninja-Debug
cmake --build --preset linux-ninja-Debug
# Run tests
cd build && ctest -VVgit clone https://github.com/LexisNexis-GHCPE/component-model-cpp.git
cd component-model-cpp
git submodule update --init --recursive
# Configure and build
cmake --preset vcpkg-VS-17
cmake --build --preset VS-17-Debug
# Run tests
cd build && ctest -C Debug -VVgit clone https://github.com/LexisNexis-GHCPE/component-model-cpp.git
cd component-model-cpp
git submodule update --init --recursive
# Configure and build
cmake --preset linux-ninja-Debug
cmake --build --preset linux-ninja-Debug
# Run tests
cd build && ctest -VVIf you prefer not to use CMake presets:
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build .
ctest -VV # Run testsThe following CMake options control what gets built:
BUILD_TESTING(default: ON) - Build unit testsBUILD_SAMPLES(default: ON) - Build sample applications demonstrating runtime integrationBUILD_GRAMMAR(default: ON) - Generate C++ code from ANTLR grammar for WIT parsing
Example:
cmake -DBUILD_TESTING=ON -DBUILD_SAMPLES=ON -DBUILD_GRAMMAR=ON ..The project includes an ANTLR grammar for parsing WebAssembly Interface Types (WIT). To generate C++ parser code:
# Enable grammar generation during configuration
cmake -DBUILD_GRAMMAR=ON ..
# Generate the code
cmake --build . --target generate-grammarRequirements:
- Java runtime (for ANTLR)
- The ANTLR jar is automatically downloaded during CMake configuration
Generated C++ files are compiled into a static library wit-grammar in the build tree that can be linked by tools. See grammar/README.md for details.
The presets build tests with GCC/Clang coverage instrumentation enabled, so generating a report is mostly a matter of running the suite and capturing the counters. On Ubuntu the full workflow looks like:
# 1. Install tooling (once per machine)
sudo apt-get update
sudo apt-get install -y lcov
# 2. Rebuild and rerun tests to refresh .gcda files
cmake --build --preset linux-ninja-Debug
cd build
ctest --output-on-failure
# 3. Capture raw coverage data
lcov --capture --directory . --output-file coverage.info
# 4. Filter out system headers, vcpkg packages, and tests (optional but recommended)
lcov --remove coverage.info '/usr/include/*' '*/vcpkg/*' '*/test/*' \
--output-file coverage.filtered.info --ignore-errors unused
# 5. Inspect the summary or render HTML
lcov --list coverage.filtered.info
genhtml coverage.filtered.info --output-directory coverage-html # optionalGenerated artifacts live in the build/ directory (coverage.info, coverage.filtered.info, and optionally coverage-html/). The same commands work on other platforms once the equivalent of lcov (or LLVM's llvm-cov) is installed.
Install cmcpp to a local directory (default: build/stage):
cmake --preset linux-ninja-Debug
cmake --build build
cmake --build build --target installOnce installed, use find_package() to integrate cmcpp:
find_package(cmcpp REQUIRED)
target_link_libraries(my_app PRIVATE cmcpp::cmcpp)Build your project:
cmake . -DCMAKE_PREFIX_PATH=/path/to/cmcpp/install
cmake --build .Generate packages with CPack:
cd build
# All default packages for your platform
cpack
# Specific formats
cpack -G TGZ # Tar.gz archive (cross-platform)
cpack -G DEB # Debian package (.deb)
cpack -G RPM # RPM package (.rpm) - requires 'rpm' package installed
cpack -G ZIP # ZIP archive (Windows)Note: To create RPM packages on Ubuntu/Debian, install the rpm package first:
sudo apt-get install -y rpmPackages include:
- Complete header-only library
- CMake config files for
find_package() wit-codegentool for generating C++ bindings from WIT files (ifBUILD_GRAMMAR=ON)
See docs/PACKAGING.md for complete packaging documentation.
The core library has no compiled library dependency. To use it in your project,
include cmcpp.hpp and either add the include directory to your include path
or install the CMake interface target:
- Copy the contents of the
includedirectory to your project. - Install via
cmake --build build --target installand usefind_package(cmcpp). - Use
vcpkgto install the library and its dependencies (planned).
Most host interactions begin by materialising an InstanceContext. This container wires together the host trap callback, string conversion routine, and the guest realloc export. Use createInstanceContext to capture those dependencies once:
cmcpp::HostTrap trap = [](const char *msg) {
throw std::runtime_error(msg ? msg : "trap");
};
cmcpp::HostUnicodeConversion convert = {}; // see test/host-util.cpp for an ICU-backed example
cmcpp::GuestRealloc realloc = [&](int ptr, int old_size, int align, int new_size) {
return guest_realloc(ptr, old_size, align, new_size);
};
auto icx = cmcpp::createInstanceContext(trap, convert, realloc);When preparing to lift or lower values, create a LiftLowerContext from the instance. Pass the guest memory span and any canonical options you need:
cmcpp::Heap heap(4096);
cmcpp::CanonicalOptions options;
options.memory = cmcpp::GuestMemory(heap.memory.data(), heap.memory.size());
options.string_encoding = cmcpp::Encoding::Utf8;
options.realloc = icx->realloc;
options.post_return = [] { /* guest cleanup */ };
options.callback = [](cmcpp::EventCode code, uint32_t index, uint32_t payload) {
std::printf("async event %u for handle %u (0x%x)\n",
static_cast<unsigned>(code), index, payload);
};
options.sync = false; // allow async continuations
auto cx = icx->createLiftLowerContext(std::move(options));
cx->inst = &component_instance;The canonical options determine whether async continuations are allowed (sync), which hook to run after a successful lowering (post_return), and how async notifications surface back to the embedder (callback). Every guest call that moves data across the ABI should use the same context until LiftLowerContext::exit_call() is invoked.
The Component Model runtime is cooperative: hosts advance work by draining a pending queue. cmcpp/runtime.hpp provides the same primitives as the canonical Python reference:
Storeowns the queue ofThreadobjects and exposesinvokeplustick().FuncInstis the callable signature hosts use to wrap guest functions.Thread::createbuilds resumable work with readiness and resume callbacks.Call::from_threadreturns a handle that supports cancellation and completion queries.Taskbridges canonical backpressure (canon_task.{return,cancel}) and ensuresComponentInstance::may_leaverules are enforced.
A minimal async call looks like this:
cmcpp::Store store;
cmcpp::FuncInst guest = [](cmcpp::Store &store,
cmcpp::SupertaskPtr,
cmcpp::OnStart on_start,
cmcpp::OnResolve on_resolve) {
auto args = std::make_shared<std::vector<std::any>>(on_start());
auto ready = std::make_shared<std::atomic<bool>>(false);
auto thread = cmcpp::Thread::create(
store,
[ready] { return ready->load(); },
[args, on_resolve](bool cancelled) {
on_resolve(cancelled ? std::nullopt : std::optional{*args});
return false; // one-shot
},
/*notify_on_cancel=*/true,
[ready] { ready->store(true); });
return cmcpp::Call::from_thread(thread);
};
auto call = store.invoke(
guest,
nullptr,
[] { return std::vector<std::any>{int32_t{7}}; },
[](std::optional<std::vector<std::any>> values) {
if (!values) { std::puts("cancelled"); return; }
std::printf("resolved with %d\n", std::any_cast<int32_t>((*values)[0]));
});
while (!call.completed()) {
store.tick();
}Call::request_cancellation() cooperatively aborts work before the next tick(), mirroring the canonical cancel semantics.
ComponentInstance manages resource tables that back the canonical canon_waitable_*, canon_stream_*, and canon_future_* entry points. Hosts typically:
- Instantiate a descriptor (
make_stream_descriptor<T>(),make_future_descriptor<T>(), etc.). - Create handles via
canon_stream_new/canon_future_new, which return packed readable/writable indices. - Join readable ends to a waitable set with
canon_waitable_join. - Poll readiness using
canon_waitable_set_poll, decoding theEventCodeand payload stored in guest memory. - Drop resources with the corresponding
canon_*_drop_*helpers once the guest is finished.
Streams and futures honour the canonical copy result payload layout, so the values copied into guest memory exactly match the spec. Cancellation helpers (canon_stream_cancel_*, canon_future_cancel_*) post events when the embedder requests termination, and the async callback registered in CanonicalOptions receives the same event triplet that the waitable set reports.
For a complete walkthrough, see the doctest suites in test/main.cpp:
- "Async runtime schedules threads" demonstrates
Store,Thread,Call, and cancellation. - "Waitable set surfaces stream readiness" polls a waitable set tied to a stream.
- "Future lifecycle completes" verifies readable/writable futures.
- "Task yield, cancel, and return" exercises backpressure and async task APIs.
Those tests are ICU-enabled and run automatically via ctest.
- Component Model design and specification: Official Component Model specification.
- wit-bindgen c++ host: C++ host support for the WebAssembly Interface Types (WIT) Bindgen tool.
