Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions crates/cpp/helper-types/wit.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <stdlib.h> // free
#include <new>
#include <span>
#include <utility> // pair

namespace wit {
/// @brief Helper class to map between IDs and resources
Expand Down Expand Up @@ -170,6 +171,79 @@ template <class T> class vector {
}
};

/// @brief A map stored as a contiguous array of key-value pairs in linear
/// memory, freed unconditionally using free.
///
/// Mirrors the canonical ABI representation of `map<K, V>` (`list<tuple<K, V>>`)
/// to enable lift without per-entry tree allocation. The container has no
/// ordering or hashing guarantees and exposes only the subset of the
/// `std::unordered_map` API that's meaningful over a flat pair buffer plus
/// the bindings-construction primitives (`allocate`, `initialize`, `leak`,
/// `drop_raw`, `data`).
template <class K, class V> class unordered_map {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why unordered_map when the entries are ordered?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name reflects WIT map<K, V> semantics, not the layout: it's a set of key/value associations with no ordering guarantee, same as std::unordered_map.

The contiguous, ordered-looking buffer is incidental, the canonical ABI is list<tuple<K, V>>, so entries land in whatever order the producer serialized them. We keep that array (instead of a tree) only so lift is allocation-free; the order isn't meaningful or guaranteed.

Calling it map would wrongly imply std::map's key-sorted semantics, which we don't provide.

public:
using entry_type = std::pair<K, V>;

private:
entry_type *data_;
size_t length;

static entry_type* empty_ptr() { return (entry_type*)alignof(entry_type); }

public:
unordered_map(unordered_map const &) = delete;
unordered_map(unordered_map &&b) : data_(b.data_), length(b.length) {
b.data_ = nullptr;
b.length = 0;
}
unordered_map &operator=(unordered_map const &) = delete;
unordered_map &operator=(unordered_map &&b) {
if (data_ && length > 0) {
for (unsigned i = 0; i < length; ++i) { data_[i].~entry_type(); }
free(data_);
}
data_ = b.data_;
length = b.length;
b.data_ = nullptr;
b.length = 0;
return *this;
}
unordered_map(entry_type *d, size_t l) : data_(d), length(l) {}
unordered_map() : data_(empty_ptr()), length() {}
entry_type const *data() const { return data_; }
entry_type *data() { return data_; }
size_t size() const { return length; }
bool empty() const { return !length; }
~unordered_map() {
if (data_ && length > 0) {
for (unsigned i = 0; i < length; ++i) { data_[i].~entry_type(); }
free((void*)data_);
}
}
// WARNING: unordered_map contains uninitialized entries; caller must
// construct them via `initialize` before the map is observed or destroyed.
static unordered_map<K, V> allocate(size_t len) {
if (!len) return unordered_map<K, V>(empty_ptr(), 0);
return unordered_map<K, V>(
(entry_type*)malloc(sizeof(entry_type) * len), len);
}
void initialize(size_t n, entry_type&& entry) {
new ((void*)(data_ + n)) entry_type(std::move(entry));
}
entry_type *leak() {
entry_type *result = data_;
data_ = nullptr;
return result;
}
static void drop_raw(void *ptr) {
if (ptr != empty_ptr()) free(ptr);
}
entry_type *begin() { return data_; }
entry_type *end() { return data_ + length; }
entry_type const *begin() const { return data_; }
entry_type const *end() const { return data_ + length; }
};

/// @brief A Resource defined within the guest (guest side)
///
/// It registers with the host and should remain in a static location.
Expand Down
184 changes: 164 additions & 20 deletions crates/cpp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,9 @@ impl Cpp {
if self.dependencies.needs_tuple {
self.include("<tuple>");
}
if self.dependencies.needs_span {
self.include("<span>");
}
if self.dependencies.needs_wit {
self.include("\"wit.h\"");
}
Expand Down Expand Up @@ -906,7 +909,7 @@ impl CppInterfaceGenerator<'_> {
TypeDefKind::Stream(_) => todo!("generate for stream"),
TypeDefKind::Handle(_) => todo!("generate for handle"),
TypeDefKind::FixedLengthList(_, _) => todo!(),
TypeDefKind::Map(_, _) => todo!(),
TypeDefKind::Map(k, v) => self.type_map(id, name, k, v, &ty.docs),
TypeDefKind::Unknown => unreachable!(),
}
}
Expand Down Expand Up @@ -1733,7 +1736,30 @@ impl CppInterfaceGenerator<'_> {
self.type_name(ty, from_namespace, flavor)
)
}
TypeDefKind::Map(_, _) => todo!(),
TypeDefKind::Map(key, value) => {
let borrowed = match flavor {
Flavor::BorrowedArgument => true,
Flavor::Argument(var) => {
matches!(var, AbiVariant::GuestImport)
|| self.r#gen.opts.api_style == APIStyle::Symmetric
}
_ => false,
};
let element_flavor = if borrowed {
Flavor::BorrowedArgument
} else {
Flavor::InStruct
};
let k = self.type_name(key, from_namespace, element_flavor);
let v = self.type_name(value, from_namespace, element_flavor);
if borrowed {
self.r#gen.dependencies.needs_span = true;
format!("std::span<std::pair<{k}, {v}> const>")
} else {
self.r#gen.dependencies.needs_wit = true;
format!("wit::unordered_map<{k}, {v}>")
}
}
TypeDefKind::Unknown => todo!(),
},
Type::ErrorContext => todo!(),
Expand Down Expand Up @@ -2258,7 +2284,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a>
_value: &wit_bindgen_core::wit_parser::Type,
_docs: &wit_bindgen_core::wit_parser::Docs,
) {
todo!("map types are not yet supported in the C++ backend")
// nothing to do here
}

fn type_builtin(
Expand Down Expand Up @@ -3483,17 +3509,19 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> {
let len = self.tempname("_len", tmp);
uwriteln!(self.src, "uint8_t* {ptr} = {};", operands[0]);
uwriteln!(self.src, "size_t {len} = {};", operands[1]);
let i = self.tempname("i", tmp);
uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{");
let size = self.r#gen.sizes.size(element);
uwriteln!(
self.src,
"uint8_t* _base = {ptr} + {i} * {size};",
size = size.format(POINTER_SIZE_EXPRESSION)
);
uwriteln!(self.src, "(void) _base;");
uwrite!(self.src, "{body}");
uwriteln!(self.src, "}}");
if !body.trim().is_empty() {
let i = self.tempname("i", tmp);
uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{");
let size = self.r#gen.sizes.size(element);
uwriteln!(
self.src,
"uint8_t* _base = {ptr} + {i} * {size};",
size = size.format(POINTER_SIZE_EXPRESSION)
);
uwriteln!(self.src, "(void) _base;");
uwrite!(self.src, "{body}");
uwriteln!(self.src, "}}");
}
uwriteln!(self.src, "if ({len} > 0) {{");
uwriteln!(self.src, "free((void*) ({ptr}));");
uwriteln!(self.src, "}}");
Expand Down Expand Up @@ -3541,12 +3569,128 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> {
}
abi::Instruction::AsyncTaskReturn { .. } => todo!(),
abi::Instruction::DropHandle { .. } => todo!(),
abi::Instruction::MapLower { .. }
| abi::Instruction::MapLift { .. }
| abi::Instruction::IterMapKey { .. }
| abi::Instruction::IterMapValue { .. }
| abi::Instruction::GuestDeallocateMap { .. } => {
todo!("map types are not yet supported in this backend")
abi::Instruction::MapLower {
key,
value,
realloc,
} => {
let tmp = self.tmp();
let body = self.blocks.pop().unwrap();
let val = format!("map{tmp}");
let ptr = format!("ptr{tmp}");
let len = format!("len{tmp}");
let entry = self.r#gen.sizes.record([*key, *value]);
let size = entry.size.format(POINTER_SIZE_EXPRESSION);
let align = entry.align.format(POINTER_SIZE_EXPRESSION);
// The canonical ABI entry layout can differ from the C++ entry
// layout (see wit-bindgen#1592), so always allocate a fresh ABI
// buffer rather than reusing the source map's storage.
self.push_str(&format!("auto&& {val} = {};\n", operands[0]));
self.push_str(&format!("auto {len} = {val}.size();\n"));
uwriteln!(
self.src,
"auto {ptr} = static_cast<{ptr_type}>({len} > 0 ? cabi_realloc(nullptr, 0, {align}, {len} * {size}) : nullptr);",
ptr_type = self.r#gen.r#gen.opts.ptr_type()
);
uwriteln!(self.src, "for (size_t i = 0; i < {len}; ++i) {{");
uwriteln!(self.src, "auto _base = {ptr} + i * {size};");
uwriteln!(self.src, "(void) _base;");
uwriteln!(self.src, "auto&& iter_entry = {val}.data()[i];");
uwriteln!(self.src, "auto&& iter_map_key = iter_entry.first;");
uwriteln!(self.src, "auto&& iter_map_value = iter_entry.second;");
uwrite!(self.src, "{}", body.0);
uwriteln!(self.src, "}}");
if realloc.is_some() {
uwriteln!(self.src, "{}.leak();", operands[0]);
}
results.push(ptr);
results.push(len);
}
abi::Instruction::MapLift { key, value, .. } => {
let body = self.blocks.pop().unwrap();
let tmp = self.tmp();
let entry = self.r#gen.sizes.record([*key, *value]);
let size = entry.size.format(POINTER_SIZE_EXPRESSION);
let flavor = if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric
&& matches!(self.variant, AbiVariant::GuestExport)
{
Flavor::BorrowedArgument
} else {
Flavor::InStruct
};
let key_type = self.r#gen.type_name(key, &self.namespace, flavor);
let value_type = self.r#gen.type_name(value, &self.namespace, flavor);
let len = format!("len{tmp}");
let base = format!("base{tmp}");
let result = format!("result{tmp}");
uwriteln!(self.src, "auto {base} = {};", operands[0]);
uwriteln!(self.src, "auto {len} = {};", operands[1]);
uwriteln!(
self.src,
"auto {result} = wit::unordered_map<{key_type}, {value_type}>::allocate({len});"
);
if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric
&& matches!(self.variant, AbiVariant::GuestExport)
{
assert!(self.needs_dealloc);
uwriteln!(self.src, "if ({len}>0) _deallocate.push_back({base});");
}
uwriteln!(self.src, "for (unsigned i=0; i<{len}; ++i) {{");
uwriteln!(self.src, "auto _base = {base} + i * {size};");
uwriteln!(self.src, "(void) _base;");
uwrite!(self.src, "{}", body.0);
let body_key = &body.1[0];
let body_value = &body.1[1];
uwriteln!(
self.src,
"{result}.initialize(i, std::make_pair({}, {}));",
move_if_necessary(body_key),
move_if_necessary(body_value)
);
uwriteln!(self.src, "}}");

if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric
&& matches!(self.variant, AbiVariant::GuestExport)
{
self.r#gen.r#gen.dependencies.needs_wit = true;
self.r#gen.r#gen.dependencies.needs_span = true;
results.push(format!(
"std::span<std::pair<{key_type}, {value_type}> const>({result}.data(), {result}.size())"
));
self.leak_on_insertion.replace(format!(
"if ({len}>0) _deallocate.push_back((void*){result}.leak());\n"
));
} else {
results.push(move_if_necessary(&result));
}
}
abi::Instruction::IterMapKey { .. } => {
results.push("iter_map_key".to_string());
}
abi::Instruction::IterMapValue { .. } => {
results.push("iter_map_value".to_string());
}
abi::Instruction::GuestDeallocateMap { key, value } => {
let (body, results) = self.blocks.pop().unwrap();
assert!(results.is_empty());
let tmp = self.tmp();
let ptr = self.tempname("_ptr", tmp);
let len = self.tempname("_len", tmp);
uwriteln!(self.src, "uint8_t* {ptr} = {};", operands[0]);
uwriteln!(self.src, "size_t {len} = {};", operands[1]);
if !body.trim().is_empty() {
let i = self.tempname("i", tmp);
uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{");
let entry = self.r#gen.sizes.record([*key, *value]);
let size = entry.size.format(POINTER_SIZE_EXPRESSION);
uwriteln!(self.src, "uint8_t* _base = {ptr} + {i} * {size};");
uwriteln!(self.src, "(void) _base;");
uwrite!(self.src, "{body}");
uwriteln!(self.src, "}}");
}
uwriteln!(self.src, "if ({len} > 0) {{");
uwriteln!(self.src, "free((void*) ({ptr}));");
uwriteln!(self.src, "}}");
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/test/src/cpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl LanguageMethods for Cpp {
return false;
}
return match name {
"issue1514-6.wit" | "named-fixed-length-list.wit" | "map.wit" => true,
"issue1514-6.wit" | "named-fixed-length-list.wit" => true,
_ => false,
} || config.async_;
}
Expand Down
Loading