diff --git a/cmd/gravity/src/codegen/exports.rs b/cmd/gravity/src/codegen/exports.rs index b414168..665d2c2 100644 --- a/cmd/gravity/src/codegen/exports.rs +++ b/cmd/gravity/src/codegen/exports.rs @@ -374,4 +374,100 @@ mod tests { but VariantLower variable is uint64), got:\n{generated}" ); } + + /// Regression test: an export whose parameters flatten to more than the + /// canonical ABI's 16 flat params is lowered *indirectly* - the host must + /// `cabi_realloc` an area, store each field into it and pass a single + /// pointer. That path emits `Instruction::Malloc`, which used to `todo!()`. + #[test] + fn test_export_indirect_params_allocates_with_realloc() { + use wit_bindgen_core::wit_parser::{Field, Record, TypeDef, TypeDefKind, TypeOwner}; + + let mut resolve = Resolve::new(); + + // A record with 17 u32 fields flattens to 17 core params, one over the + // limit of 16, which forces indirect parameter lowering. + let record_def = TypeDef { + name: Some("wide".to_string()), + kind: TypeDefKind::Record(Record { + fields: (0..17) + .map(|i| Field { + name: format!("field{i}"), + ty: Type::U32, + docs: Default::default(), + span: Default::default(), + }) + .collect(), + }), + owner: TypeOwner::None, + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + let record_id = resolve.types.alloc(record_def); + + let func = Function { + name: "take_wide".to_string(), + kind: FunctionKind::Freestanding, + params: vec![Param { + name: "wide".to_string(), + ty: Type::Id(record_id), + span: Default::default(), + }], + result: Some(Type::U32), + docs: Default::default(), + stability: Default::default(), + span: Default::default(), + }; + + let world = World { + name: "test-world".to_string(), + imports: [].into(), + exports: [( + WorldKey::Name("take-wide".to_string()), + WorldItem::Function(func.clone()), + )] + .into(), + docs: Default::default(), + stability: Default::default(), + includes: Default::default(), + span: Default::default(), + package: None, + }; + + let mut sizes = SizeAlign::default(); + sizes.fill(&resolve); + let instance = GoIdentifier::public("TestInstance"); + + let config = ExportConfig { + instance: &instance, + world: &world, + resolve: &resolve, + sizes: &sizes, + }; + + let generator = ExportGenerator::new(config); + let mut tokens = Tokens::new(); + generator.generate_function(&func, &mut tokens); + + let generated = tokens.to_string().unwrap(); + println!("Generated wide-record function:\n{}", generated); + + // The param area is allocated through the guest's `cabi_realloc` with + // the record's alignment (4) and size (17 * 4 = 68 bytes). + assert!( + generated.contains("ExportedFunction(\"cabi_realloc\").Call(ctx, 0, 0, 4, 68)"), + "indirect params must allocate the param area via cabi_realloc, got:\n{generated}" + ); + // Each field is stored into that area, and the pointer is the only + // argument passed to the exported wasm function. + assert!( + generated.contains("Memory().WriteUint32Le"), + "indirect params must be stored into the allocated area, got:\n{generated}" + ); + assert!( + generated.contains("ExportedFunction(\"take_wide\").Call(ctx, uint64(ptr"), + "the wasm export must be called with the param area pointer, got:\n{generated}" + ); + } } diff --git a/cmd/gravity/src/codegen/func.rs b/cmd/gravity/src/codegen/func.rs index 35f552c..b0fedd2 100644 --- a/cmd/gravity/src/codegen/func.rs +++ b/cmd/gravity/src/codegen/func.rs @@ -1444,7 +1444,53 @@ impl Bindgen for Func<'_> { results.push(Operand::SingleValue(enum_value.to_string())); } - Instruction::Malloc { .. } => todo!("implement instruction: {inst:?}"), + Instruction::Malloc { + realloc, + size, + align, + } => { + // Emitted when a function's parameters flatten to more than the + // canonical ABI's limit of 16 flat params. The caller allocates + // an area in the guest's memory with `cabi_realloc`, stores each + // param into it and passes the pointer as the only argument. + // + // TODO(#58): Support additional ArchitectureSize + let size = size.size_wasm32(); + let align = align.align_wasm32(); + let tmp = self.tmp(); + let result = &format!("result{tmp}"); + let err = &format!("err{tmp}"); + let default = &format!("default{tmp}"); + let ptr = &format!("ptr{tmp}"); + + quote_in! { self.body => + $['\r'] + $(comment(&["Allocate the area holding the indirectly passed parameters"])) + $result, $err := $module_handle.ExportedFunction($(quoted(*realloc))).Call(ctx, 0, 0, $align, $size) + $(match &self.result { + GoResult::Anon(GoType::ValueOrError(typ)) => { + if $err != nil { + var $default $(typ.as_ref()) + return $default, $err + } + } + GoResult::Anon(GoType::Error) => { + if $err != nil { + return $err + } + } + GoResult::Anon(_) | GoResult::Empty => { + $(comment(&["The return type doesn't contain an error so we panic if one is encountered"])) + if $err != nil { + panic($err) + } + } + }) + $ptr := uint32($result[0]) + }; + + results.push(Operand::SingleValue(ptr.into())); + } Instruction::HandleLower { .. } | Instruction::HandleLift { .. } => { todo!("implement resources: {inst:?}") } diff --git a/cmd/gravity/tests/cmd/instructions.stdout b/cmd/gravity/tests/cmd/instructions.stdout index 7793b2a..3766664 100644 --- a/cmd/gravity/tests/cmd/instructions.stdout +++ b/cmd/gravity/tests/cmd/instructions.stdout @@ -26,6 +26,25 @@ const ( Three enumValues = iota ) +type WideParams struct { + A uint32 + B uint32 + C uint32 + D uint32 + E uint32 + F uint32 + G uint32 + H uint32 + I uint32 + J uint32 + K uint32 + L uint32 + M uint32 + N uint32 + O uint32 + P string +} + type InstructionsFactory struct { runtime wazero.Runtime module wazero.CompiledModule @@ -255,3 +274,110 @@ func (i *InstructionsInstance) EnumInput( } } +func (i *InstructionsInstance) IndirectParams( + ctx context.Context, + val WideParams, +) string { + arg0 := val + // Allocate the area holding the indirectly passed parameters + result0, err0 := i.module.ExportedFunction("cabi_realloc").Call(ctx, 0, 0, 4, 68) + // The return type doesn't contain an error so we panic if one is encountered + if err0 != nil { + panic(err0) + } + ptr0 := uint32(result0[0]) + a1 := arg0.A + b1 := arg0.B + c1 := arg0.C + d1 := arg0.D + e1 := arg0.E + f1 := arg0.F + g1 := arg0.G + h1 := arg0.H + i1 := arg0.I + j1 := arg0.J + k1 := arg0.K + l1 := arg0.L + m1 := arg0.M + n1 := arg0.N + o1 := arg0.O + p1 := arg0.P + result2 := uint32(a1) + i.module.Memory().WriteUint32Le(ptr0+0, result2) + result3 := uint32(b1) + i.module.Memory().WriteUint32Le(ptr0+4, result3) + result4 := uint32(c1) + i.module.Memory().WriteUint32Le(ptr0+8, result4) + result5 := uint32(d1) + i.module.Memory().WriteUint32Le(ptr0+12, result5) + result6 := uint32(e1) + i.module.Memory().WriteUint32Le(ptr0+16, result6) + result7 := uint32(f1) + i.module.Memory().WriteUint32Le(ptr0+20, result7) + result8 := uint32(g1) + i.module.Memory().WriteUint32Le(ptr0+24, result8) + result9 := uint32(h1) + i.module.Memory().WriteUint32Le(ptr0+28, result9) + result10 := uint32(i1) + i.module.Memory().WriteUint32Le(ptr0+32, result10) + result11 := uint32(j1) + i.module.Memory().WriteUint32Le(ptr0+36, result11) + result12 := uint32(k1) + i.module.Memory().WriteUint32Le(ptr0+40, result12) + result13 := uint32(l1) + i.module.Memory().WriteUint32Le(ptr0+44, result13) + result14 := uint32(m1) + i.module.Memory().WriteUint32Le(ptr0+48, result14) + result15 := uint32(n1) + i.module.Memory().WriteUint32Le(ptr0+52, result15) + result16 := uint32(o1) + i.module.Memory().WriteUint32Le(ptr0+56, result16) + memory17 := i.module.Memory() + realloc17 := i.module.ExportedFunction("cabi_realloc") + ptr17, len17, err17 := writeString(ctx, p1, memory17, realloc17) + // The return type doesn't contain an error so we panic if one is encountered + if err17 != nil { + panic(err17) + } + i.module.Memory().WriteUint32Le(ptr0+64, uint32(len17)) + i.module.Memory().WriteUint32Le(ptr0+60, uint32(ptr17)) + raw18, err18 := i.module.ExportedFunction("indirect-params").Call(ctx, uint64(ptr0)) + // The return type doesn't contain an error so we panic if one is encountered + if err18 != nil { + panic(err18) + } + + // The cleanup via `cabi_post_*` cleans up the memory in the guest. By + // deferring this, we ensure that no memory is corrupted before the function + // is done accessing it. + defer func() { + if postFn := i.module.ExportedFunction("cabi_post_indirect-params"); postFn != nil { + if _, err := postFn.Call(ctx, raw18...); err != nil { + // If we get an error during cleanup, something really bad is + // going on, so we panic. Also, you can't return the error from + // the `defer` + panic(errors.New("failed to cleanup")) + } + } + }() + + results18 := raw18[0] + ptr19, ok19 := i.module.Memory().ReadUint32Le(uint32(results18 + 0)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok19 { + panic(errors.New("failed to read pointer from memory")) + } + len20, ok20 := i.module.Memory().ReadUint32Le(uint32(results18 + 4)) + // The return type doesn't contain an error so we panic if one is encountered + if !ok20 { + panic(errors.New("failed to read length from memory")) + } + buf21, ok21 := i.module.Memory().Read(ptr19, len20) + // The return type doesn't contain an error so we panic if one is encountered + if !ok21 { + panic(errors.New("failed to read bytes from memory")) + } + str21 := string(buf21) + return str21 +} + diff --git a/examples/instructions/instructions_test.go b/examples/instructions/instructions_test.go index d46a311..f29d046 100644 --- a/examples/instructions/instructions_test.go +++ b/examples/instructions/instructions_test.go @@ -240,3 +240,46 @@ func Test_EnumInput(t *testing.T) { ins.EnumInput(t.Context(), Two) ins.EnumInput(t.Context(), Three) } + +// Test_IndirectParams covers the indirect parameter path of the canonical ABI. +// WideParams flattens to 17 core params, one over the limit of 16, so the host +// must allocate an area with cabi_realloc (Instruction::Malloc), store every +// field into it and pass a single pointer. The guest echoes every field back so +// a wrong size, alignment or store offset shows up as a mismatched string. +func Test_IndirectParams(t *testing.T) { + fac, err := NewInstructionsFactory(t.Context()) + if err != nil { + t.Fatal(err) + } + defer fac.Close(t.Context()) + + ins, err := fac.Instantiate(t.Context()) + if err != nil { + t.Fatal(err) + } + defer ins.Close(t.Context()) + + val := WideParams{ + A: 1, + B: 2, + C: 3, + D: 4, + E: 5, + F: 6, + G: 7, + H: 8, + I: 9, + J: 10, + K: 11, + L: 12, + M: 13, + N: 14, + O: math.MaxUint32, + P: "wide", + } + + want := "wide:1,2,3,4,5,6,7,8,9,10,11,12,13,14,4294967295" + if got := ins.IndirectParams(t.Context(), val); got != want { + t.Errorf("expected: %q, but got: %q", want, got) + } +} diff --git a/examples/instructions/src/lib.rs b/examples/instructions/src/lib.rs index 0e2ce64..ac7dae2 100644 --- a/examples/instructions/src/lib.rs +++ b/examples/instructions/src/lib.rs @@ -45,4 +45,26 @@ impl Guest for InstructionsWorld { EnumValues::One | EnumValues::Two | EnumValues::Three )); } + + fn indirect_params(val: WideParams) -> String { + let WideParams { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p, + } = val; + format!("{p}:{a},{b},{c},{d},{e},{f},{g},{h},{i},{j},{k},{l},{m},{n},{o}") + } } diff --git a/examples/instructions/wit/instructions.wit b/examples/instructions/wit/instructions.wit index 404b7aa..78feed6 100644 --- a/examples/instructions/wit/instructions.wit +++ b/examples/instructions/wit/instructions.wit @@ -24,4 +24,29 @@ world instructions { } export enum-input: func(val: enum-values); + + // A record whose fields flatten to 17 core params, one over the canonical + // ABI's limit of 16. Passing it forces the *indirect* param path: the host + // allocates an area with `cabi_realloc` (Instruction::Malloc), stores every + // field into it and passes a single pointer. + record wide-params { + a: u32, + b: u32, + c: u32, + d: u32, + e: u32, + f: u32, + g: u32, + h: u32, + i: u32, + j: u32, + k: u32, + l: u32, + m: u32, + n: u32, + o: u32, + p: string, + } + + export indirect-params: func(val: wide-params) -> string; }