Skip to content

C#: pass single-field exposed structs through DllImport as their scalar (IL2CPP wasm ABI) - #48

Closed
Grantim wants to merge 2 commits into
masterfrom
fix/single-field-exposed-struct-pinvoke-abi
Closed

C#: pass single-field exposed structs through DllImport as their scalar (IL2CPP wasm ABI)#48
Grantim wants to merge 2 commits into
masterfrom
fix/single-field-exposed-struct-pinvoke-abi

Conversation

@Grantim

@Grantim Grantim commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Unity WebGL (IL2CPP) builds of MeshLib's wasm bindings link with 622 wasm-ld: warning: function signature mismatch, one for every C API function returning an MR::*Id:

wasm-ld: warning: function signature mismatch: MR_EdgeId_undirected
>>> defined as (i32, i32) -> void   in GameAssembly.a(<IL2CPP object>)      <- IL2CPP call site
>>> defined as (i32) -> i32         in libMeshLibC2.a(unity_0_cxx.cxx.o)    <- MeshLib C API

Those calls trap at runtime. Worse, the ~1000 C API functions that take an Id by value get no warning at all (same i32 on both sides), but IL2CPP passes a pointer to a stack copy where the C function reads the id.

Root cause: IL2CPP emits every C# struct as struct { union { struct { ... }; uint8_t padding[N]; }; }, and Clang's wasm32 ABI only flattens single-element structs. So a one-field exposed struct becomes an aggregate on the IL2CPP side (sret return, pass by pointer), while the C side returns/accepts a plain int. Multi-field structs (Vector3f, Box3f, ...) are unaffected: both sides already pass them indirectly. Reproduced with Unity's own emcc, and the 622 warned functions are exactly the set of P/Invokes returning an Id type.

Fix

If an exposed struct has exactly one non-static field, and that field is an arithmetic type other than bool or an enum, the C# generator now declares the DllImport parameters/return as that scalar and converts at the boundary (v.x on the way in, new T {x = ...} on the way out). A scalar has the same ABI on every runtime and target. The public C# API and the C API are unchanged (in C both spellings have the same ABI on all supported platforms). Pointer paths (this, in, defaulted params) are untouched.

The two constructor emission paths (this = ... and the Box wrapper's *(T *)_UnderlyingPtr = ...) now go through the return binding so they compose with this; for other types they produce the same text as before.

Verification

  • Regenerated all test outputs with the parser (gcc 14 + the llvm-22.1.8 libs, like CI). The diff is confined to single-field exposed structs (ConvCtorExposed, ExposedLayoutB, ExposedLayoutC, DeclOrder::*), including constructors and the std::function<ExposedLayoutC(ExposedLayoutC)> callback wrapper.
  • dotnet build of the three generated C# test projects: 0 warnings.
  • Added test_exposed_c() to cover a by-value parameter and return value in a plain function.

Follow-up in MeshLib: bump mrbind and regenerate the C# bindings; the Unity wasm link should then be free of function signature mismatch warnings.

🤖 Generated with Claude Code

Grantim and others added 2 commits September 2, 2026 15:31
Unity's IL2CPP wraps every C# struct into a union with padding, and on wasm32 Clang only passes/returns single-element structs as plain scalars, so the IL2CPP call site used a hidden return pointer and pass-by-pointer while the C library used a plain `int` (`wasm-ld: function signature mismatch`, traps or garbage at runtime). Declaring the `DllImport` with the field's scalar type has the same ABI on both sides; the C API and the public C# API are unchanged. Structs with several fields already agreed on both sides and are left as is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds `test_exposed_c()` taking and returning a single-field exposed struct by value, and regenerates the C and C# test outputs. The C# diff shows the new `DllImport` shape: `int` in place of the struct, `.x` at the call sites, and `new T {x = ...}` on return, including constructors and the `std::function` callback wrapper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Grantim

Grantim commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #49, which fixes the same IL2CPP/wasm ABI mismatch with a much smaller change (LayoutKind.Sequential for single-field exposed structs instead of rewriting the DllImport signatures).

@Grantim Grantim closed this Sep 2, 2026
@Grantim
Grantim deleted the fix/single-field-exposed-struct-pinvoke-abi branch September 2, 2026 13:04
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.

1 participant