Skip to content

BeefRT: a dynamic call for wasm, so reflection Invoke works there - #2509

Merged
bfiete merged 1 commit into
beefytech:masterfrom
jayrulez:fix/wasm-reflection-invoke-has-no-ffi
Sep 22, 2026
Merged

bfiete merged 1 commit into
beefytech:masterfrom
jayrulez:fix/wasm-reflection-invoke-has-no-ffi

Conversation

@jayrulez

Copy link
Copy Markdown
Contributor

MethodInfo.Invoke is the only dynamic call Beef has, and it goes through FFILIB. The wasm runtime is built with BF_DISABLE_FFI, where PrepCif return FFIResult_NoFFI and Call is no-op, so every Invoke on wasm failed.

Symptom: Type.GetCustomAttribute has to construct the attribute to return it, so an attribute plainly present read back as absent, while HasCustomAttribute (a type id compare, no call) still said yes.

wasm does not need libffi. libffi is complicated because ABIs like SysV x86-64 scatter a struct's fields across registers, so a call has to be assembled per signature. clang's wasm32 ABI passes EVERY struct indirectly as one pointer and returns one through a hidden sret pointer prepended to the arguments: measured on the table entries, an 8 byte and a 24 byte struct both take arity 1, and returning either takes arity params+1. So every case is one number per argument, which is what a call through the function table already does.

PrepCif fills the cif record, Call maps each FFIType.TypeKind to a slot kind and dispatches through EM_JS to wasmTable.get(fp).apply. BuildContext exports wasmTable on every wasm link, without which the runtime's JS cannot see it.

BF_DISABLE_FFI still means no libffi rather than no FFI, so it keeps working for any other target without one. ClosureAlloc stays unimplemented, and its comment now says why: corlib binds the allocator and not ffi_prep_closure_loc.

Covers the shapes that can go wrong: i32, f32, f64, an i64 past 32 bits, a struct by value, a 24 byte struct by value, a struct returned by value, a void return, and the GetCustomAttribute case that surfaced it.

@jayrulez

Copy link
Copy Markdown
Contributor Author

I'll rebase this

@jayrulez
jayrulez force-pushed the fix/wasm-reflection-invoke-has-no-ffi branch 4 times, most recently from a100a94 to d19e595 Compare September 21, 2026 22:26
@bfiete

bfiete commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Well, I don't really love there being a big block of emscripten-specific code thrown into a core BeefRT file. At minimum it needs to be separated off into some wasm-specific support file. I don't love that I don't have any idea whether this is actually a full and proper implementation, as it seems there could easily be cases it doesn't cover... but I guess that's still better than zero invocation support.

MethodInfo.Invoke is the only dynamic call Beef has, and it goes through FFILIB.
The wasm runtime is built with BF_DISABLE_FFI, where PrepCif answered
FFIResult_NoFFI and Call did nothing, so every Invoke on wasm failed. The
visible symptom was not Invoke: Type.GetCustomAttribute has to CONSTRUCT the
attribute to return it, so an attribute plainly present read back as absent,
while HasCustomAttribute (a type id compare, no call) still said yes.

wasm does not need libffi. A call through the function table takes one number
per parameter, and corlib has already decided the calling convention before
FFILIB sees anything: a struct parameter is splatted into its fields exactly as
the compiler passed it, or handed over as a pointer, and a struct return becomes
a void call with the destination pointer as a leading argument. So for Beef's
own methods the layer only ever sees scalars and pointers, which is what
wasmTable.get(fp).apply already takes.

The implementation lives in BeefySysLib/platform/wasm/WasmFFI.cpp, beside
WasmCommon.cpp, and Internal.cpp only dispatches to it under
BF_DISABLE_FFI && __EMSCRIPTEN__. Its header comment states what it covers and
what it does not: a Struct FFIType from user code driving System.FFI directly is
passed by address per clang's wasm32 ABI, except that clang passes a
single-scalar struct as the scalar, which is not special cased; LongDouble is
treated as f64; wasm32 only. BuildContext exports wasmTable on every wasm link,
without which the runtime's JS cannot see the table. ClosureAlloc stays
unimplemented, as on every backend.

BF_DISABLE_FFI still means no libffi rather than no FFI, so it keeps working for
any other target without one.

Tests cover i32, f32, f64, an i64 past 32 bits, a struct by value, a 24 byte
struct by value, a struct returned by value, a void return, and the
GetCustomAttribute case that surfaced it.
@jayrulez
jayrulez force-pushed the fix/wasm-reflection-invoke-has-no-ffi branch from d19e595 to 50799e8 Compare September 22, 2026 01:50
@jayrulez

Copy link
Copy Markdown
Contributor Author

Refactored

@bfiete
bfiete merged commit 57e108a into beefytech:master Sep 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants