From 61bad335208490b32b2af83ff9fd0cf8ab6ed2ad Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 26 Jul 2026 17:13:32 -0400 Subject: [PATCH 1/2] fix(cpp): bind result payloads by reference during lowering ResultLower moved the payload into a block-scoped local, so when lowering a result argument into an import, the payload's heap data (e.g. map entries or strings) was freed at the end of the ok/err block, before the import call consumed the flat buffer. Binding by rvalue reference keeps the data owned by the operand, which outlives the call, matching how VariantLower already binds case payloads. The export direction is unaffected: it transfers ownership into the flat buffer with leak() and frees in cabi_post, and leaking through the reference behaves the same. Signed-off-by: Yordis Prieto --- crates/cpp/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 2d7b30099..27f6fdf31 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -3300,13 +3300,17 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { &self.namespace, Flavor::InStruct, ); + // Bind payloads by reference: the payload's heap data must + // stay owned by `op0` (which outlives the lowered call). A + // by-value binding would free that data at the end of the + // `if`/`else` block below, before the flat ABI consumes it. let bind_ok = if let Some(_ok) = result.ok.as_ref() { - format!("{ok_ty} {ok_payload} = std::move({op0}).value();") + format!("{ok_ty}&& {ok_payload} = std::move({op0}).value();") } else { String::new() }; let bind_err = if let Some(_err) = result.err.as_ref() { - format!("{err_ty} {err_payload} = std::move({op0}).error();") + format!("{err_ty}&& {err_payload} = std::move({op0}).error();") } else { String::new() }; From a0b1c5e7859310c927b4dc05fac9eac1e0156489 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 26 Jul 2026 17:13:39 -0400 Subject: [PATCH 2/2] test(cpp): cover map runtime roundtrips Restores the C++ map runtime coverage deferred in #1590 and adds the import direction, which never existed. The runner exercises the span-based borrowed-map lowering plus owned maps inside records, variants, results, and tuples. The C++ harness now links a core module with --skip-wit-component and componentizes with the workspace's wit-component, mirroring the C harness. This keeps the wasi-sdk's bundled wasm-component-ld out of the WIT processing path, which predates the component-model map encoding and was the reason #1590 dropped its runtime test. Signed-off-by: Yordis Prieto --- crates/test/src/cpp.rs | 16 ++- tests/runtime/map/runner.cpp | 229 +++++++++++++++++++++++++++++++++++ tests/runtime/map/test.cpp | 76 ++++++++++++ 3 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 tests/runtime/map/runner.cpp create mode 100644 tests/runtime/map/test.cpp diff --git a/crates/test/src/cpp.rs b/crates/test/src/cpp.rs index d33cefa11..436116797 100644 --- a/crates/test/src/cpp.rs +++ b/crates/test/src/cpp.rs @@ -146,8 +146,13 @@ impl LanguageMethods for Cpp { .arg(&bindings_object); runner.run_command(&mut cmd)?; - // Now compile the runner's source code to with the above object and the - // component-type object into a final component. + // Now link the runner's source code with the above object and the + // component-type object into a core module, then componentize with + // the workspace's `wit-component` (mirroring the C harness). This + // skips the WIT processing of the wasi-sdk's bundled + // `wasm-component-ld`, which may predate component-model features + // used by the embedded metadata (e.g. `map`). + let output = compile.output.with_extension("core.wasm"); let mut cmd = Command::new(compiler); cmd.arg(&compile.component.path) .arg(&bindings_object) @@ -170,12 +175,17 @@ impl LanguageMethods for Cpp { .arg("-std=c++20") .arg("-g") .arg("-o") - .arg(&compile.output); + .arg(&output); for flag in Vec::from(config.cflags) { cmd.arg(flag); } cmd.arg("-mexec-model=reactor"); + cmd.arg("-Wl,--skip-wit-component"); runner.run_command(&mut cmd)?; + + runner + .convert_p1_to_component(&output, compile) + .with_context(|| format!("failed to convert {output:?}"))?; Ok(()) } diff --git a/tests/runtime/map/runner.cpp b/tests/runtime/map/runner.cpp new file mode 100644 index 000000000..7d8281c0f --- /dev/null +++ b/tests/runtime/map/runner.cpp @@ -0,0 +1,229 @@ +//@ wasmtime-flags = '-Wcomponent-model-map' + +#include +#include + +#include +#include +#include + +namespace to_test = ::test::maps::to_test; + +using wit::string; +using wit::unordered_map; + +static bool str_eq(string const& a, std::string_view b) { return a.get_view() == b; } + +template +static V const* find(unordered_map const& m, std::string_view key) { + for (auto const& [k, v] : m) { + if (k.get_view() == key) { + return &v; + } + } + return nullptr; +} + +template +static V const* find(unordered_map const& m, uint32_t key) { + for (auto const& [k, v] : m) { + if (k == key) { + return &v; + } + } + return nullptr; +} + +static unordered_map make_names( + std::initializer_list> entries) { + auto result = unordered_map::allocate(entries.size()); + size_t i = 0; + for (auto const& [key, value] : entries) { + result.initialize(i++, std::make_pair(key, string::from_view(value))); + } + return result; +} + +static void test_named_roundtrip() { + std::vector> input{{1, "uno"}, {2, "two"}}; + auto result = to_test::NamedRoundtrip(input); + assert(result.size() == 2); + auto uno = find(result, "uno"); + assert(uno && *uno == 1); + auto two = find(result, "two"); + assert(two && *two == 2); +} + +static void test_bytes_roundtrip() { + uint8_t const world_bytes[] = {'w', 'o', 'r', 'l', 'd'}; + uint8_t const bin_bytes[] = {0, 1, 2}; + std::vector>> input{ + {"hello", world_bytes}, + {"bin", bin_bytes}, + }; + auto result = to_test::BytesRoundtrip(input); + assert(result.size() == 2); + auto hello = find(result, "hello"); + assert(hello && hello->size() == 5); + assert(memcmp(hello->data(), "world", 5) == 0); + auto bin = find(result, "bin"); + assert(bin && bin->size() == 3); + assert((*bin)[0] == 0 && (*bin)[1] == 1 && (*bin)[2] == 2); +} + +static void test_empty_roundtrip() { + auto result = to_test::EmptyRoundtrip({}); + assert(result.empty()); +} + +static void test_option_roundtrip() { + std::vector>> input{ + {"some", 42}, + {"none", std::nullopt}, + }; + auto result = to_test::OptionRoundtrip(input); + assert(result.size() == 2); + auto some = find(result, "some"); + assert(some && some->has_value() && **some == 42); + auto none = find(result, "none"); + assert(none && !none->has_value()); +} + +static void test_record_roundtrip() { + to_test::LabeledEntry input{ + string::from_view("test-label"), + make_names({{10, "ten"}, {20, "twenty"}}), + }; + auto result = to_test::RecordRoundtrip(std::move(input)); + assert(str_eq(result.label, "test-label")); + assert(result.values.size() == 2); + auto ten = find(result.values, 10); + assert(ten && str_eq(*ten, "ten")); + auto twenty = find(result.values, 20); + assert(twenty && str_eq(*twenty, "twenty")); +} + +static void test_inline_roundtrip() { + std::vector> input{{1, "one"}, {2, "two"}}; + auto result = to_test::InlineRoundtrip(input); + assert(result.size() == 2); + auto one = find(result, "one"); + assert(one && *one == 1); + auto two = find(result, "two"); + assert(two && *two == 2); +} + +static void test_large_roundtrip() { + size_t const n = 100; + std::vector storage; + storage.reserve(n); + std::vector> input; + input.reserve(n); + for (size_t i = 0; i < n; i++) { + storage.push_back("value-" + std::to_string(i)); + input.emplace_back(uint32_t(i), storage.back()); + } + auto result = to_test::LargeRoundtrip(input); + assert(result.size() == n); + auto value = find(result, 42); + assert(value && str_eq(*value, "value-42")); +} + +static void test_multi_param_roundtrip() { + std::vector> names{{1, "one"}, {2, "two"}}; + uint8_t const payload[] = {42}; + std::vector>> bytes{{"key", payload}}; + auto [ids, bytes_out] = to_test::MultiParamRoundtrip(names, bytes); + assert(ids.size() == 2); + auto one = find(ids, "one"); + assert(one && *one == 1); + auto two = find(ids, "two"); + assert(two && *two == 2); + assert(bytes_out.size() == 1); + auto key = find(bytes_out, "key"); + assert(key && key->size() == 1 && (*key)[0] == 42); +} + +static void test_nested_roundtrip() { + std::vector> inner_a{{1, "one"}, {2, "two"}}; + std::vector> inner_b{{10, "ten"}}; + std::vector const>>> + outer{ + {"group-a", inner_a}, + {"group-b", inner_b}, + }; + auto result = to_test::NestedRoundtrip(outer); + assert(result.size() == 2); + auto group_a = find(result, "group-a"); + assert(group_a && group_a->size() == 2); + auto two = find(*group_a, 2); + assert(two && str_eq(*two, "two")); + auto group_b = find(result, "group-b"); + assert(group_b && group_b->size() == 1); + auto ten = find(*group_b, 10); + assert(ten && str_eq(*ten, "ten")); +} + +static void test_variant_roundtrip() { + to_test::MapOrString map_input{to_test::MapOrString::AsMap{make_names({{1, "one"}})}}; + auto map_result = to_test::VariantRoundtrip(std::move(map_input)); + auto* as_map = std::get_if(&map_result.variants); + assert(as_map && as_map->value.size() == 1); + auto one = find(as_map->value, 1); + assert(one && str_eq(*one, "one")); + + to_test::MapOrString string_input{to_test::MapOrString::AsString{string::from_view("hello")}}; + auto string_result = to_test::VariantRoundtrip(std::move(string_input)); + auto* as_string = std::get_if(&string_result.variants); + assert(as_string && str_eq(as_string->value, "hello")); +} + +static void test_result_roundtrip() { + std::expected, string> ok_input{make_names({{5, "five"}})}; + auto ok_result = to_test::ResultRoundtrip(std::move(ok_input)); + assert(ok_result.has_value()); + assert(ok_result->size() == 1); + auto five = find(*ok_result, 5); + assert(five && str_eq(*five, "five")); + + std::expected, string> err_input{ + std::unexpected(string::from_view("bad input"))}; + auto err_result = to_test::ResultRoundtrip(std::move(err_input)); + assert(!err_result.has_value()); + assert(str_eq(err_result.error(), "bad input")); +} + +static void test_tuple_roundtrip() { + std::vector> entries{{7, "seven"}}; + auto result = to_test::TupleRoundtrip(std::make_tuple( + std::span const>(entries), uint64_t(42))); + auto& [values, number] = result; + assert(values.size() == 1); + auto seven = find(values, 7); + assert(seven && str_eq(*seven, "seven")); + assert(number == 42); +} + +static void test_single_entry_roundtrip() { + std::vector> input{{99, "ninety-nine"}}; + auto result = to_test::SingleEntryRoundtrip(input); + assert(result.size() == 1); + auto value = find(result, 99); + assert(value && str_eq(*value, "ninety-nine")); +} + +void exports::runner::Run() { + test_named_roundtrip(); + test_bytes_roundtrip(); + test_empty_roundtrip(); + test_option_roundtrip(); + test_record_roundtrip(); + test_inline_roundtrip(); + test_large_roundtrip(); + test_multi_param_roundtrip(); + test_nested_roundtrip(); + test_variant_roundtrip(); + test_result_roundtrip(); + test_tuple_roundtrip(); + test_single_entry_roundtrip(); +} diff --git a/tests/runtime/map/test.cpp b/tests/runtime/map/test.cpp new file mode 100644 index 000000000..f06f781b6 --- /dev/null +++ b/tests/runtime/map/test.cpp @@ -0,0 +1,76 @@ +#include + +namespace test_exports = ::exports::test::maps::to_test; + +using wit::string; +using wit::unordered_map; +using wit::vector; + +static unordered_map invert(unordered_map a) { + auto result = unordered_map::allocate(a.size()); + size_t i = 0; + for (auto& [id, name] : a) { + result.initialize(i++, std::make_pair(std::move(name), id)); + } + return result; +} + +unordered_map test_exports::NamedRoundtrip(unordered_map a) { + return invert(std::move(a)); +} + +unordered_map> test_exports::BytesRoundtrip( + unordered_map> a) { + return a; +} + +unordered_map test_exports::EmptyRoundtrip(unordered_map a) { + return a; +} + +unordered_map> test_exports::OptionRoundtrip( + unordered_map> a) { + return a; +} + +test_exports::LabeledEntry test_exports::RecordRoundtrip(test_exports::LabeledEntry a) { + return a; +} + +unordered_map test_exports::InlineRoundtrip(unordered_map a) { + return invert(std::move(a)); +} + +unordered_map test_exports::LargeRoundtrip(unordered_map a) { + return a; +} + +std::tuple, unordered_map>> +test_exports::MultiParamRoundtrip(unordered_map a, + unordered_map> b) { + return std::make_tuple(invert(std::move(a)), std::move(b)); +} + +unordered_map> test_exports::NestedRoundtrip( + unordered_map> a) { + return a; +} + +test_exports::MapOrString test_exports::VariantRoundtrip(test_exports::MapOrString a) { + return a; +} + +std::expected, string> test_exports::ResultRoundtrip( + std::expected, string> a) { + return a; +} + +std::tuple, uint64_t> test_exports::TupleRoundtrip( + std::tuple, uint64_t> a) { + return a; +} + +unordered_map test_exports::SingleEntryRoundtrip( + unordered_map a) { + return a; +}