dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.57k stars 4.55k forks source link

Using Raylib APIs in Blazor WebAssembly #96384

Open DarkHarlock opened 6 months ago

DarkHarlock commented 6 months ago

Hello,

I'm currently facing a challenge while migrating code that uses Raylib from a C# .NET 8 console application to Blazor WebAssembly. Initially, everything seemed to work fine until I encountered a set of APIs (such as Font GetFontDefault(void)) that return "by value" structures larger than a simple pointer.

On Windows, these functions seem to operate without any issues. However, the problem arises when I attempt to use them in Blazor WebAssembly.

To simplify and better understand the issue, I've created a reproducible example using a NativeFileReference file containing the following C code:

typedef struct Buffer64 { char f1[64]; } Buffer64;

Buffer64 GetBuffer64(void) {
    Buffer64 result = { }; 
    for (char i = 0; i < 64; i++) {
        result.f1[i] = 0b00100001;
    }
    return result;
}

For interfacing with the APIs, I've defined the following import:

[System.Runtime.CompilerServices.InlineArray(64)]
public struct Buffer64
{
    private byte _element0; 
}

public static unsafe partial class TestCase
{
    [LibraryImport("testcode", EntryPoint = "GetBuffer64")]
    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
    public static partial Buffer64 GetBuffer64();
}

Running this code, just like with Raylib, the call to TestCase.GetBuffer64() method results in a System.InvalidProgramException.

Is this intended to be a supported pinvoke method definition? I've searched for information on this but haven't found anything significant, aside from some limitations in delegate marshaling to native code and support for varargs.

Subsequently, I conducted another test by moving the return parameter and treating it as the first argument of the method, leaving unchanged the native code, as shown in the following example:

public static unsafe partial class TestCase
{
    [LibraryImport("testcode", EntryPoint = "GetBuffer64")]
    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
    public static partial void GetBuffer64(Buffer64* buffer);
}

and then used as:

Buffer64 buffer = default;
TestCase.GetBuffer64(&buffer)

With this change, things started working, and the buffer contains the correct information.

My subsequent question is whether this "trick" (move ret value as first parameter when sizeof(T) > sizeof(nint)) should be considered supported, and so safe to be used as workaround/solution to this problem, or it is merely a coincidence related to implementation details.

Thank you for your support and help in clarifying this issue.

ghost commented 6 months ago

Tagging subscribers to this area: @dotnet/interop-contrib See info in area-owners.md if you want to be subscribed.

Issue Details
Hello, I'm currently facing a challenge while migrating code that uses Raylib from a C# .NET 8 console application to Blazor WebAssembly. Initially, everything seemed to work fine until I encountered a set of APIs (such as `Font GetFontDefault(void)`) that return "by value" structures larger than a simple pointer. On Windows, these functions seem to operate without any issues. However, the problem arises when I attempt to use them in Blazor WebAssembly. To simplify and better understand the issue, I've created a reproducible [example](https://github.com/DarkHarlock/BlazorNativeIssueRepro) using a `NativeFileReference` file containing the following C code: ```C typedef struct Buffer64 { char f1[64]; } Buffer64; Buffer64 GetBuffer64(void) { Buffer64 result = { }; for (char i = 0; i < 64; i++) { result.f1[i] = 0b00100001; } return result; } ``` For interfacing with the APIs, I've defined the following import: ```csharp [System.Runtime.CompilerServices.InlineArray(64)] public struct Buffer64 { private byte _element0; } public static unsafe partial class TestCase { [LibraryImport("testcode", EntryPoint = "GetBuffer64")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial Buffer64 GetBuffer64(); } ``` Running this code, just like with Raylib, the call to `TestCase.GetBuffer64()` method results in a `System.InvalidProgramException`. Is this intended to be a supported pinvoke method definition? I've searched for information on this but haven't found anything significant, aside from some limitations in delegate marshaling to native code and support for varargs. Subsequently, I conducted another test by moving the return parameter and treating it as the first argument of the method, leaving unchanged the native code, as shown in the following example: ```csharp public static unsafe partial class TestCase { [LibraryImport("testcode", EntryPoint = "GetBuffer64")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial void GetBuffer64(Buffer64* buffer); } ``` and then used as: ```csharp Buffer64 buffer = default; TestCase.GetBuffer64(&buffer) ``` With this change, things started working, and the buffer contains the correct information. My subsequent question is whether this "trick" (move ret value as first parameter when sizeof(T) > sizeof(nint)) should be considered supported, and so safe to be used as workaround/solution to this problem, or it is merely a coincidence related to implementation details. Thank you for your support and help in clarifying this issue.
Author: DarkHarlock
Assignees: -
Labels: `area-System.Runtime.InteropServices`, `untriaged`
Milestone: -
ghost commented 6 months ago

Tagging subscribers to 'arch-wasm': @lewing See info in area-owners.md if you want to be subscribed.

Issue Details
Hello, I'm currently facing a challenge while migrating code that uses Raylib from a C# .NET 8 console application to Blazor WebAssembly. Initially, everything seemed to work fine until I encountered a set of APIs (such as `Font GetFontDefault(void)`) that return "by value" structures larger than a simple pointer. On Windows, these functions seem to operate without any issues. However, the problem arises when I attempt to use them in Blazor WebAssembly. To simplify and better understand the issue, I've created a reproducible [example](https://github.com/DarkHarlock/BlazorNativeIssueRepro) using a `NativeFileReference` file containing the following C code: ```C typedef struct Buffer64 { char f1[64]; } Buffer64; Buffer64 GetBuffer64(void) { Buffer64 result = { }; for (char i = 0; i < 64; i++) { result.f1[i] = 0b00100001; } return result; } ``` For interfacing with the APIs, I've defined the following import: ```csharp [System.Runtime.CompilerServices.InlineArray(64)] public struct Buffer64 { private byte _element0; } public static unsafe partial class TestCase { [LibraryImport("testcode", EntryPoint = "GetBuffer64")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial Buffer64 GetBuffer64(); } ``` Running this code, just like with Raylib, the call to `TestCase.GetBuffer64()` method results in a `System.InvalidProgramException`. Is this intended to be a supported pinvoke method definition? I've searched for information on this but haven't found anything significant, aside from some limitations in delegate marshaling to native code and support for varargs. Subsequently, I conducted another test by moving the return parameter and treating it as the first argument of the method, leaving unchanged the native code, as shown in the following example: ```csharp public static unsafe partial class TestCase { [LibraryImport("testcode", EntryPoint = "GetBuffer64")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial void GetBuffer64(Buffer64* buffer); } ``` and then used as: ```csharp Buffer64 buffer = default; TestCase.GetBuffer64(&buffer) ``` With this change, things started working, and the buffer contains the correct information. My subsequent question is whether this "trick" (move ret value as first parameter when sizeof(T) > sizeof(nint)) should be considered supported, and so safe to be used as workaround/solution to this problem, or it is merely a coincidence related to implementation details. Thank you for your support and help in clarifying this issue.
Author: DarkHarlock
Assignees: -
Labels: `arch-wasm`, `area-System.Runtime.InteropServices`, `untriaged`
Milestone: -
jkotas commented 6 months ago

Running this code, just like with Raylib, the call to TestCase.GetBuffer64() method results in a System.InvalidProgramException. Is this intended to be a supported pinvoke method definition?

Yes, this is intended to be a supported pinvoke method definition. Looks like you have run into wasm-specific bug.

My subsequent question is whether this "trick" (move ret value as first parameter when sizeof(T) > sizeof(nint)) should be considered supported

This trick is not generally supported. It is non-portable code. It will happen to work for some platform and method signature combinations, but not others.

kg commented 6 months ago

I suspect the problem is InlineArray. In this case a fixed array in the struct definition might work. I've added this feature to a tracking issue, and will try to get to it

DarkHarlock commented 6 months ago

The inline array was just used to replicate the issue in an easy way, but the original problem was from code like the following from Raylib:

typedef struct Font {
    int baseSize;           // Base size (default chars height)
    int glyphCount;         // Number of glyph characters
    int glyphPadding;       // Padding around the glyph characters
    Texture2D texture;      // Texture atlas containing the glyphs
    Rectangle *recs;        // Rectangles in texture for the glyphs
    GlyphInfo *glyphs;      // Glyphs info data
} Font;

RLAPI Font GetFontDefault(void);  

that is imported in C# with the following binding:

public struct Font
{
    public int BaseSize;
    public int GlyphCount;
    public int GlyphPadding;
    public Texture2D Texture;
    public unsafe Rectangle* Recs;
    public unsafe GlyphInfo* Glyphs;
}

[DllImport("raylib", CallingConvention = CallingConvention.Cdecl)]
public static extern Font GetFontDefault();

However, reading that the issue might be with the InlineArray, I updated the code to be like:

public struct Buffer64
{
    private byte _element0;
    private byte _element1;
    private byte _element2;
    ...
    private byte _element63;

    public readonly byte this[int index]
    {
        get
        {
            switch (index)
            {
                case 0: return _element0;
                ...
                case 63: return _element63;
            }
            throw new ArgumentOutOfRangeException(nameof(index));
        }
    }
}

with no changes on C side. In this case, instead of getting an System.InvalidProgramException, I now encounter a runtime error and the entire Blazor host unloads. I moved again on the original Raylib project to verify what of the 2 error I get, and it was runtime error, the same that I get without the InlineArray.

Sorry, with the initial tests, I stopped at the point that it didn't work in both cases, not realizing that the kind of error was different.

As a recap:

Note that the trick of passing it as ref on first argument still works with both struct types.

kg commented 6 months ago

Thanks for testing more. We'll look into this too.

kg commented 6 months ago

Is Texture2D a struct with members? Or is it an alias for a pointer/scalar

DarkHarlock commented 6 months ago

Is Texture2D a struct with members? Or is it an alias for a pointer/scalar

Texture2d is defined as a struct with members:

// Texture, tex data stored in GPU memory (VRAM)
typedef struct Texture {
    unsigned int id;        // OpenGL texture id
    int width;              // Texture base width
    int height;             // Texture base height
    int mipmaps;            // Mipmap levels, 1 by default
    int format;             // Data format (PixelFormat type)
} Texture;

// Texture2D, same as Texture
typedef Texture Texture2D;
kg commented 4 months ago

Just to update on this: