Skip to content
Closed
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
6 changes: 6 additions & 0 deletions docs/generating_csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ Eventually we might add translation from the Doxygen comment annotations to thos

This should be enough to at least display those comments in IDEs, but any `@...` doxygen tags will be left as is, instead of being translated into their proper XML form.

## Exposed structs and Unity's IL2CPP on WebAssembly

Structs exposed via `--expose-as-struct` (see [the C docs](/docs/generating_c.md#expose-simple-structs-as-structs)) are emitted as blittable C# structs with `LayoutKind.Explicit` and explicit field offsets, except structs with a single field, which use `LayoutKind.Sequential`.

The exception exists because of Unity's IL2CPP on WebAssembly. IL2CPP compiles explicit-layout structs to a union with padding, and on wasm32 Clang only passes and returns *single-element* structs as plain scalars, which such a union isn't. So for a one-field struct the IL2CPP-compiled call site would return it through a hidden pointer and pass it by pointer, while the C library returns and accepts a plain scalar (`wasm-ld` reports `function signature mismatch`, and the calls trap or read garbage at runtime). A sequential one-field struct compiles to a plain C struct, which has the same ABI as the C side. Structs with more fields don't have this problem, since both sides pass those indirectly.

## Distributing the C# bindings as a Nuget package

This is not a full explanation, but a rought outline of what you need to do.
Expand Down
25 changes: 20 additions & 5 deletions src/generators/csharp/generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5952,13 +5952,27 @@ namespace mrbind::CSharp
WriteComment(file, comment);
}

// Exposed structs with a single field use `LayoutKind.Sequential` instead of `LayoutKind.Explicit` (for one field both layouts are trivially the same).
// This is for Unity's IL2CPP on wasm32: it compiles explicit-layout structs to a union with padding, and Clang's wasm32 ABI only passes and returns
// *single-element* structs as plain scalars, which that union isn't. So with `Explicit` the IL2CPP-compiled call site returns a one-field struct
// through a hidden pointer and passes it by pointer, while the C side returns and accepts a plain scalar (`wasm-ld` warns about `function signature mismatch`,
// and the calls trap or read garbage). A sequential one-field struct compiles to a plain C struct, which matches. Multi-field structs are indirect on both sides anyway.
const bool exposed_struct_is_sequential = is_exposed_struct_by_value && std::ranges::count_if(class_desc.fields, [](const CInterop::ClassField &field){return !field.is_static;}) == 1;

// The struct attributes.
if (is_exposed_struct_by_value)
{
// There's also `Pack = ...` parameter. It looks related to alignment, but the docs say that it only affects
// the automatic field layout if that's enabled, and not the alignment of the entire struct.
// Instead I'm going to assume that it aligns by the largest field size, and check that below.
file.WriteString("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = " + std::to_string(class_desc.size_and_alignment.value().size) + ")]\n");
if (exposed_struct_is_sequential)
{
file.WriteString("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]\n");
}
else
{
// There's also `Pack = ...` parameter. It looks related to alignment, but the docs say that it only affects
// the automatic field layout if that's enabled, and not the alignment of the entire struct.
// Instead I'm going to assume that it aligns by the largest field size, and check that below.
file.WriteString("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = " + std::to_string(class_desc.size_and_alignment.value().size) + ")]\n");
}
}

// The class header.
Expand Down Expand Up @@ -6619,7 +6633,8 @@ namespace mrbind::CSharp
// Write the field by value, if we're in the by-value part.
if (is_exposed_struct_by_value)
{
const std::string offset_attr = "[System.Runtime.InteropServices.FieldOffset(" + std::to_string(field.layout.value().byte_offset) + ")]\n";
// No offsets in sequential structs, `FieldOffset` is only allowed with `LayoutKind.Explicit`.
const std::string offset_attr = exposed_struct_is_sequential ? "" : "[System.Runtime.InteropServices.FieldOffset(" + std::to_string(field.layout.value().byte_offset) + ")]\n";

// Write the field itself.
if (is_bool)
Expand Down
3 changes: 3 additions & 0 deletions test/input/MR/test_csharp.h
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,9 @@ namespace MR::CSharp
int x;
};

// Single-field exposed structs use `LayoutKind.Sequential` in C#. This tests passing and returning one by value.
inline ExposedLayoutC test_exposed_c(ExposedLayoutC a, ExposedLayoutC b = {}) {a.x += b.x; return a;}


// Test various array members.
struct ArrayMembers
Expand Down
5 changes: 5 additions & 0 deletions test/output_c/include/MR/test_csharp.h
Original file line number Diff line number Diff line change
Expand Up @@ -6312,6 +6312,11 @@ MR_C_API MR_CSharp_ExposedLayoutB MR_CSharp_ExposedLayoutB_Construct_1(const MR_
/// Parameter `_2` can not be null. It is a single object.
MR_C_API bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, const MR_CSharp_ExposedLayoutB *_2);

// Single-field exposed structs use `LayoutKind.Sequential` in C#. This tests passing and returning one by value.
/// Generated from function `MR::CSharp::test_exposed_c`.
/// Parameter `b` has a default argument: `{}`, pass a null pointer to use it.
MR_C_API MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b);

/// Returns a pointer to a member variable of class `MR::CSharp::ArrayMembers` named `i`.
/// Parameter `_this` can not be null. It is a single object.
/// The returned pointer will never be null. It is non-owning, do NOT destroy it.
Expand Down
12 changes: 12 additions & 0 deletions test/output_c/source/MR/test_csharp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9550,6 +9550,18 @@ bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, con
) // MRBINDC_TRY
}

MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b)
{
MRBINDC_TRY(
using namespace MR;
using namespace CSharp;
return MRBINDC_BIT_CAST((MR_CSharp_ExposedLayoutC), ::MR::CSharp::test_exposed_c(
MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), a),
(b ? MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), *b) : MR::CSharp::ExposedLayoutC(MR::CSharp::ExposedLayoutC{}))
));
) // MRBINDC_TRY
}

const int *MR_CSharp_ArrayMembers_Get_i(const MR_CSharp_ArrayMembers *_this)
{
return std::addressof(((_this ? void() : MRBINDC_THROW("Parameter `_this` can not be null.", void)), *(const MR::CSharp::ArrayMembers *)(_this)).i);
Expand Down
5 changes: 5 additions & 0 deletions test/output_c_fixed_typedefs/include/MR/test_csharp.h
Original file line number Diff line number Diff line change
Expand Up @@ -6128,6 +6128,11 @@ MR_C_API MR_CSharp_ExposedLayoutB MR_CSharp_ExposedLayoutB_Construct_1(const MR_
// Parameter `_2` can not be null. It is a single object.
MR_C_API bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, const MR_CSharp_ExposedLayoutB *_2);

// Single-field exposed structs use `LayoutKind.Sequential` in C#. This tests passing and returning one by value.
// Generated from function `MR::CSharp::test_exposed_c`.
// Parameter `b` has a default argument: `{}`, pass a null pointer to use it.
MR_C_API MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b);

// Returns a pointer to a member variable of class `MR::CSharp::ArrayMembers` named `i`.
// Parameter `_this` can not be null. It is a single object.
// The returned pointer will never be null. It is non-owning, do NOT destroy it.
Expand Down
8 changes: 8 additions & 0 deletions test/output_c_fixed_typedefs/source/MR/test_csharp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6882,6 +6882,14 @@ bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, con
);
}

MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b)
{
return MRBINDC_BIT_CAST((MR_CSharp_ExposedLayoutC), ::MR::CSharp::test_exposed_c(
MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), a),
(b ? MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), *b) : MR::CSharp::ExposedLayoutC(MR::CSharp::ExposedLayoutC{}))
));
}

const int32_t *MR_CSharp_ArrayMembers_Get_i(const MR_CSharp_ArrayMembers *_this)
{
return std::addressof(((_this ? void() : MRBINDC_THROW("Parameter `_this` can not be null.", void)), *(const MR::CSharp::ArrayMembers *)(_this)).i);
Expand Down
5 changes: 5 additions & 0 deletions test/output_c_fixed_typedefs_64_only/include/MR/test_csharp.h
Original file line number Diff line number Diff line change
Expand Up @@ -6311,6 +6311,11 @@ MR_C_API MR_CSharp_ExposedLayoutB MR_CSharp_ExposedLayoutB_Construct_1(const MR_
/// Parameter `_2` can not be null. It is a single object.
MR_C_API bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, const MR_CSharp_ExposedLayoutB *_2);

// Single-field exposed structs use `LayoutKind.Sequential` in C#. This tests passing and returning one by value.
/// Generated from function `MR::CSharp::test_exposed_c`.
/// Parameter `b` has a default argument: `{}`, pass a null pointer to use it.
MR_C_API MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b);

/// Returns a pointer to a member variable of class `MR::CSharp::ArrayMembers` named `i`.
/// Parameter `_this` can not be null. It is a single object.
/// The returned pointer will never be null. It is non-owning, do NOT destroy it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7138,6 +7138,14 @@ bool MR_C_equal_MR_CSharp_ExposedLayoutB(const MR_CSharp_ExposedLayoutB *_1, con
);
}

MR_CSharp_ExposedLayoutC MR_CSharp_test_exposed_c(MR_CSharp_ExposedLayoutC a, const MR_CSharp_ExposedLayoutC *b)
{
return MRBINDC_BIT_CAST((MR_CSharp_ExposedLayoutC), ::MR::CSharp::test_exposed_c(
MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), a),
(b ? MRBINDC_BIT_CAST((MR::CSharp::ExposedLayoutC), *b) : MR::CSharp::ExposedLayoutC(MR::CSharp::ExposedLayoutC{}))
));
}

const int *MR_CSharp_ArrayMembers_Get_i(const MR_CSharp_ArrayMembers *_this)
{
return std::addressof(((_this ? void() : MRBINDC_THROW("Parameter `_this` can not be null.", void)), *(const MR::CSharp::ArrayMembers *)(_this)).i);
Expand Down
28 changes: 18 additions & 10 deletions test/output_csharp/src/MR/test_csharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13697,10 +13697,9 @@ public unsafe Box_ConvCtorExposed(int _1) : this(null, is_owning: true)
// A converting ctor in an exposed struct.
/// Generated from class `MR::CSharp::ConvCtorExposed`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ConvCtorExposed
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int x;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -15836,10 +15835,9 @@ public unsafe Box_ExposedLayoutB(MR.CS.CSharp.Const_A _1) : this(null, is_owning

/// Generated from class `MR::CSharp::ExposedLayoutB`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ExposedLayoutB : System.IEquatable<MR.CS.CSharp.ExposedLayoutB>
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int x;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -16067,10 +16065,9 @@ public unsafe Box_ExposedLayoutC(ConstBox_ExposedLayoutC _other) : this(null, is
// Just a simple exposed struct to test other things.
/// Generated from class `MR::CSharp::ExposedLayoutC`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ExposedLayoutC
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int x;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -16967,10 +16964,9 @@ public unsafe Box_A(ConstBox_A _other) : this(null, is_owning: true)

/// Generated from class `MR::CSharp::NameConflictsExposed::A`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct A
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int x;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -17564,10 +17560,9 @@ public unsafe void Foo(int _1)
// Test that we don't produce the const and non-const overloads of the same function under the same name in C#, as that would be a compilation error in C#.
/// Generated from class `MR::CSharp::ConstNonconstConflicts`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct ConstNonconstConflicts
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int x;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -25651,6 +25646,19 @@ public static unsafe ref readonly MR.CS.CSharp.ExposedLayoutSh TestExposedCref(i
if (__c_ret is not null) return *__c_ret; else return null;
}

// Single-field exposed structs use `LayoutKind.Sequential` in C#. This tests passing and returning one by value.
/// Generated from function `MR::CSharp::test_exposed_c`.
/// Parameter `b` defaults to `{}`.
public static unsafe MR.CS.CSharp.ExposedLayoutC TestExposedC(MR.CS.CSharp.ExposedLayoutC a, MR.CS.CSharp._InOpt_ExposedLayoutC b = default)
{
[System.Runtime.InteropServices.DllImport("bleh", EntryPoint = "MR_CSharp_test_exposed_c", ExactSpelling = true)]
extern static MR.CS.CSharp.ExposedLayoutC __MR_CSharp_test_exposed_c(MR.CS.CSharp.ExposedLayoutC a, MR.CS.CSharp.ExposedLayoutC *b);
MR.CS.Misc._Exceptions.Prepare();
var __c_ret = __MR_CSharp_test_exposed_c(a, b.HasValue ? &b.Object : null);
MR.CS.Misc._Exceptions.ThrowIfNeeded();
return __c_ret;
}

/// Generated from function `MR::CSharp::test_optint`.
/// Parameter `b` defaults to `default_optint`.
/// Parameter `c` defaults to `default_optint`.
Expand Down
12 changes: 4 additions & 8 deletions test/output_csharp/src/MR/test_declaration_order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ public unsafe MR.CS.DeclOrder.A.E D()
// Here all classes are whitelisted using `--expose-as-struct`.
/// Generated from class `MR::DeclOrder::A`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct A
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int blah;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -249,10 +248,9 @@ public unsafe MR.CS.DeclOrder.A.E B_()

/// Generated from class `MR::DeclOrder::A::B`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct B
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int bleh;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -523,10 +521,9 @@ public unsafe MR.CS.DeclOrder.C_True Blah()

/// Generated from class `MR::DeclOrder::C<false>`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct C_False
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int bleh;

/// Copy contents from a wrapper class to this struct.
Expand Down Expand Up @@ -709,10 +706,9 @@ public unsafe MR.CS.DeclOrder.C_False Blah()

/// Generated from class `MR::DeclOrder::C<true>`.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 4)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct C_True
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int bleh;

/// Copy contents from a wrapper class to this struct.
Expand Down
3 changes: 1 addition & 2 deletions test/output_csharp/src/std_array_int_43.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ public unsafe Box_Array_Int_43(ConstBox_Array_Int_43 _other) : this(null, is_own

/// A fixed-size array of `int` of size 43.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 172)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Array_Int_43
{
[System.Runtime.InteropServices.FieldOffset(0)]
public MR.CS.ArrayInt43 elems;

/// Copy contents from a wrapper class to this struct.
Expand Down
3 changes: 1 addition & 2 deletions test/output_csharp/src/std_array_int_array_4_array_3_5.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,9 @@ public unsafe Box_Array_IntArray4Array3_5(ConstBox_Array_IntArray4Array3_5 _othe

/// A fixed-size array of `int[3][4]` of size 5.
/// This is the by-value version of the struct.
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit, Size = 240)]
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Array_IntArray4Array3_5
{
[System.Runtime.InteropServices.FieldOffset(0)]
public MR.CS.ArrayInt5_3_4 elems;

/// Copy contents from a wrapper class to this struct.
Expand Down
Loading