Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 96 additions & 0 deletions cmd/gravity/src/codegen/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}
}
48 changes: 47 additions & 1 deletion cmd/gravity/src/codegen/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}")
}
Expand Down
126 changes: 126 additions & 0 deletions cmd/gravity/tests/cmd/instructions.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

43 changes: 43 additions & 0 deletions examples/instructions/instructions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
22 changes: 22 additions & 0 deletions examples/instructions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
}
25 changes: 25 additions & 0 deletions examples/instructions/wit/instructions.wit
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}