Skip to content

Commit d860036

Browse files
authored
feat(csharp): add map type support (#1603)
* feat(csharp): add map type support Dictionary<K,V> provides a familiar, zero-dependency host representation for WIT maps in C#, and unblocks runtime interop with components that use them without requiring a custom collection type. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * fix(csharp): emit component_type.o for linker The `map<K, V>` WIT syntax is a gated feature that the default wit-parser used by wasm-component-ld's `--component-type` flag cannot parse, so the text-based component type path was blocking any world that uses maps. Switching to the same object-file approach already used by the C and C++ backends lets the component type flow through as a custom section without round-tripping through the text parser. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * fix(csharp): embed component type via binary WIT package Earlier attempt used `NativeFileReference` on the component-type object, but NativeAOT's LLVM pipeline did not propagate the custom section into the final module, leaving every C# component import unresolvable. Using a binary-encoded WIT package (which `wasm-component-ld --component-type` accepts alongside the text form) preserves the existing linker path and also lets map<K,V> round-trip since decoding skips the default WIT parser that rejects gated features. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * fix(csharp): force-link component-type object for NativeAOT Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * fix(csharp): defer map runtime tests until toolchain support lands Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * refactor(csharp): drop unused component-type object plumbing Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> * chore(csharp): revert unrelated Cargo.lock drift No Cargo.toml changed on this branch, so the lockfile should match main. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com> --------- Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 6f489db commit d860036

4 files changed

Lines changed: 205 additions & 17 deletions

File tree

crates/csharp/src/function.rs

Lines changed: 193 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
10171017
results: block_results,
10181018
element: block_element,
10191019
base,
1020+
..
10201021
} = self.blocks.pop().unwrap();
10211022
assert!(block_results.is_empty());
10221023

@@ -1491,6 +1492,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
14911492
results: block_results,
14921493
base,
14931494
element: _,
1495+
..
14941496
} = self.blocks.pop().unwrap();
14951497
assert!(block_results.is_empty());
14961498

@@ -1756,18 +1758,193 @@ impl Bindgen for FunctionBindgen<'_, '_> {
17561758
self.interface_gen.csharp_gen.needs_async_support = true;
17571759
}
17581760

1761+
Instruction::MapLower {
1762+
key,
1763+
value,
1764+
realloc,
1765+
} => {
1766+
let Block {
1767+
body,
1768+
results: block_results,
1769+
base,
1770+
map_key,
1771+
map_value,
1772+
..
1773+
} = self.blocks.pop().unwrap();
1774+
assert!(block_results.is_empty());
1775+
1776+
let map = &operands[0];
1777+
let entry = self
1778+
.interface_gen
1779+
.csharp_gen
1780+
.sizes
1781+
.record([*key, *value].iter().copied());
1782+
let size = entry.size.size_wasm32();
1783+
let align = entry.align.align_wasm32();
1784+
let key_ty = self.interface_gen.type_name_with_qualifier(key, true);
1785+
let value_ty = self.interface_gen.type_name_with_qualifier(value, true);
1786+
1787+
let index = self.locals.tmp("index");
1788+
let address = self.locals.tmp("address");
1789+
let buffer_size = self.locals.tmp("bufferSize");
1790+
let entry_var = self.locals.tmp("entry");
1791+
1792+
let (array_size, element_type) =
1793+
crate::world_generator::dotnet_aligned_array(size, align);
1794+
let ret_area = self.locals.tmp("retArea");
1795+
1796+
let array_size = if align > 1 {
1797+
format!("{array_size} * {map}.Count + 1")
1798+
} else {
1799+
format!("{array_size} * {map}.Count")
1800+
};
1801+
1802+
match realloc {
1803+
None => {
1804+
self.needs_cleanup = true;
1805+
self.interface_gen.csharp_gen.needs_align_stack_ptr = true;
1806+
uwrite!(
1807+
self.src,
1808+
"
1809+
void* {address};
1810+
if (({size} * {map}.Count) < 1024) {{
1811+
var {ret_area} = stackalloc {element_type}[{array_size}];
1812+
{address} = MemoryHelper.AlignStackPtr({ret_area}, {align});
1813+
}}
1814+
else
1815+
{{
1816+
var {buffer_size} = {size} * (nuint){map}.Count;
1817+
{address} = global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc({buffer_size}, {align});
1818+
cleanups.Add(() => global::System.Runtime.InteropServices.NativeMemory.AlignedFree({address}));
1819+
}}
1820+
"
1821+
);
1822+
}
1823+
Some(_) => {
1824+
uwrite!(
1825+
self.src,
1826+
"
1827+
var {buffer_size} = {size} * (nuint){map}.Count;
1828+
void* {address} = global::System.Runtime.InteropServices.NativeMemory.AlignedAlloc({buffer_size}, {align});
1829+
"
1830+
);
1831+
}
1832+
}
1833+
1834+
uwrite!(
1835+
self.src,
1836+
"
1837+
int {index} = 0;
1838+
foreach (var {entry_var} in {map}) {{
1839+
{key_ty} {map_key} = {entry_var}.Key;
1840+
{value_ty} {map_value} = {entry_var}.Value;
1841+
int {base} = (int){address} + ({index} * {size});
1842+
{body}
1843+
++{index};
1844+
}}
1845+
"
1846+
);
1847+
1848+
results.push(format!("(int){address}"));
1849+
results.push(format!("{map}.Count"));
1850+
}
1851+
1852+
Instruction::MapLift { key, value, .. } => {
1853+
let Block {
1854+
body,
1855+
results: block_results,
1856+
base,
1857+
..
1858+
} = self.blocks.pop().unwrap();
1859+
let address = &operands[0];
1860+
let length = &operands[1];
1861+
let map = self.locals.tmp("map");
1862+
let key_ty = self.interface_gen.type_name_with_qualifier(key, true);
1863+
let value_ty = self.interface_gen.type_name_with_qualifier(value, true);
1864+
let entry = self
1865+
.interface_gen
1866+
.csharp_gen
1867+
.sizes
1868+
.record([*key, *value].iter().copied());
1869+
let size = entry.size.size_wasm32();
1870+
let index = self.locals.tmp("index");
1871+
1872+
let body_key = &block_results[0];
1873+
let body_value = &block_results[1];
1874+
1875+
uwrite!(
1876+
self.src,
1877+
"
1878+
var {map} = new global::System.Collections.Generic.Dictionary<{key_ty}, {value_ty}>((int){length});
1879+
for (int {index} = 0; {index} < {length}; ++{index}) {{
1880+
nint {base} = {address} + ({index} * {size});
1881+
{body}
1882+
{map}[{body_key}] = {body_value};
1883+
}}
1884+
1885+
if ({length} > 0) {{
1886+
global::System.Runtime.InteropServices.NativeMemory.Free((void*){address});
1887+
}}
1888+
"
1889+
);
1890+
1891+
results.push(map);
1892+
}
1893+
1894+
Instruction::IterMapKey { .. } => {
1895+
results.push(self.block_storage.last().unwrap().map_key.clone())
1896+
}
1897+
1898+
Instruction::IterMapValue { .. } => {
1899+
results.push(self.block_storage.last().unwrap().map_value.clone())
1900+
}
1901+
1902+
Instruction::GuestDeallocateMap { key, value } => {
1903+
let Block {
1904+
body,
1905+
results: block_results,
1906+
base,
1907+
..
1908+
} = self.blocks.pop().unwrap();
1909+
assert!(block_results.is_empty());
1910+
1911+
let address = &operands[0];
1912+
let length = &operands[1];
1913+
let entry = self
1914+
.interface_gen
1915+
.csharp_gen
1916+
.sizes
1917+
.record([*key, *value].iter().copied());
1918+
let size = entry.size.size_wasm32();
1919+
1920+
if !body.trim().is_empty() {
1921+
let index = self.locals.tmp("index");
1922+
1923+
uwrite!(
1924+
self.src,
1925+
"
1926+
for (int {index} = 0; {index} < {length}; ++{index}) {{
1927+
int {base} = (int){address} + ({index} * {size});
1928+
{body}
1929+
}}
1930+
"
1931+
);
1932+
}
1933+
1934+
uwriteln!(
1935+
self.src,
1936+
r#"global::System.Runtime.InteropServices.NativeMemory.Free((void*){});"#,
1937+
operands[0]
1938+
);
1939+
}
1940+
17591941
Instruction::ErrorContextLower { .. }
17601942
| Instruction::ErrorContextLift { .. }
17611943
| Instruction::DropHandle { .. }
17621944
| Instruction::FixedLengthListLift { .. }
17631945
| Instruction::FixedLengthListLower { .. }
17641946
| Instruction::FixedLengthListLowerToMemory { .. }
1765-
| Instruction::FixedLengthListLiftFromMemory { .. }
1766-
| Instruction::MapLower { .. }
1767-
| Instruction::MapLift { .. }
1768-
| Instruction::IterMapKey { .. }
1769-
| Instruction::IterMapValue { .. }
1770-
| Instruction::GuestDeallocateMap { .. } => {
1947+
| Instruction::FixedLengthListLiftFromMemory { .. } => {
17711948
dbg!(inst);
17721949
todo!()
17731950
}
@@ -1843,6 +2020,8 @@ impl Bindgen for FunctionBindgen<'_, '_> {
18432020
body: mem::take(&mut self.src),
18442021
element: self.locals.tmp("element"),
18452022
base: self.locals.tmp("basePtr"),
2023+
map_key: self.locals.tmp("mapKey"),
2024+
map_value: self.locals.tmp("mapValue"),
18462025
});
18472026

18482027
self.is_block = true;
@@ -1853,13 +2032,17 @@ impl Bindgen for FunctionBindgen<'_, '_> {
18532032
body,
18542033
element,
18552034
base,
2035+
map_key,
2036+
map_value,
18562037
} = self.block_storage.pop().unwrap();
18572038

18582039
self.blocks.push(Block {
18592040
body: mem::replace(&mut self.src, body),
18602041
results: mem::take(operands),
18612042
element,
18622043
base,
2044+
map_key,
2045+
map_value,
18632046
});
18642047
self.is_block = false;
18652048
}
@@ -1940,6 +2123,8 @@ struct Block {
19402123
results: Vec<String>,
19412124
element: String,
19422125
base: String,
2126+
map_key: String,
2127+
map_value: String,
19432128
}
19442129

19452130
struct Fixed {
@@ -1951,6 +2136,8 @@ struct BlockStorage {
19512136
body: String,
19522137
element: String,
19532138
base: String,
2139+
map_key: String,
2140+
map_value: String,
19542141
}
19552142

19562143
#[derive(Clone)]

crates/csharp/src/interface.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ impl InterfaceGenerator<'_> {
138138
TypeDefKind::Option(t) => self.type_option(type_id, typedef_name, t, &type_def.docs),
139139
TypeDefKind::Record(t) => self.type_record(type_id, typedef_name, t, &type_def.docs),
140140
TypeDefKind::List(t) => self.type_list(type_id, typedef_name, t, &type_def.docs),
141+
TypeDefKind::Map(key, value) => {
142+
self.type_map(type_id, typedef_name, key, value, &type_def.docs)
143+
}
141144
TypeDefKind::Variant(t) => self.type_variant(type_id, typedef_name, t, &type_def.docs),
142145
TypeDefKind::Result(t) => self.type_result(type_id, typedef_name, t, &type_def.docs),
143146
TypeDefKind::Handle(_) => {
@@ -1297,6 +1300,13 @@ var {async_status_var} = {raw_name}({wasm_params});
12971300
};
12981301
return format!("StreamReader{generic_type}").to_owned();
12991302
}
1303+
TypeDefKind::Map(key, value) => {
1304+
format!(
1305+
"global::System.Collections.Generic.Dictionary<{}, {}>",
1306+
self.type_name_with_qualifier(key, qualifier),
1307+
self.type_name_with_qualifier(value, qualifier)
1308+
)
1309+
}
13001310
_ => {
13011311
if let Some(name) = &ty.name {
13021312
format!(
@@ -1916,8 +1926,8 @@ impl<'a> CoreInterfaceGenerator<'a> for InterfaceGenerator<'a> {
19161926
todo!("named fixed-length list types are not yet supported in the C# backend")
19171927
}
19181928

1919-
fn type_map(&mut self, _id: TypeId, _name: &str, _key: &Type, _value: &Type, _docs: &Docs) {
1920-
todo!("map types are not yet supported in the C# backend")
1929+
fn type_map(&mut self, id: TypeId, _name: &str, _key: &Type, _value: &Type, _docs: &Docs) {
1930+
self.type_name(&Type::Id(id));
19211931
}
19221932

19231933
fn type_builtin(&mut self, _id: TypeId, _name: &str, _ty: &Type, _docs: &Docs) {

crates/csharp/src/world_generator.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -825,14 +825,6 @@ impl WorldGenerator for CSharp {
825825
);
826826
}
827827

828-
// For the time being, we generate both a .wit file and a .o file to
829-
// represent the component type. Newer releases of the .NET runtime
830-
// will be able to use the former, but older ones will need the
831-
// latter.
832-
//
833-
// TODO: stop generating the .o file once a new-enough release is
834-
// available for us to test using only the .wit file.
835-
836828
{
837829
// When generating a WIT file, we first round-trip through the
838830
// binary encoding. This has the effect of flattening any

crates/test/src/csharp.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ impl LanguageMethods for Csharp {
4949
| "import-export-resource.wit"
5050
| "issue-1433.wit"
5151
| "named-fixed-length-list.wit"
52-
| "map.wit"
5352
)
5453
}
5554

0 commit comments

Comments
 (0)