dotnet / runtime

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

[wasm] Marshaling bool within struct with [MarshalAs(UnmanagedType.U1)] throws System.InvalidProgramException #107546

Open drasticactions opened 1 month ago

drasticactions commented 1 month ago

Description

I am still determining if this is a runtime bug, but since this code works everywhere except Blazor WASM, I have to ask.

I am working on a program with a native library. It includes the following struct:

https://github.com/drasticactions/DA.Whisper/blob/wasm-bug/src/DA.Whisper/NativeMethods.g.cs#L380C5-L391C6

 [StructLayout(LayoutKind.Sequential)]
  public unsafe partial struct whisper_context_params
  {
      [MarshalAs(UnmanagedType.U1)] public bool use_gpu;
      [MarshalAs(UnmanagedType.U1)] public bool flash_attn;
      public int gpu_device;
      [MarshalAs(UnmanagedType.U1)] public bool dtw_token_timestamps;
      public whisper_alignment_heads_preset dtw_aheads_preset;
      public int dtw_n_top;
      public whisper_aheads dtw_aheads;
      public nuint dtw_mem_size;
  }

This can be retrieved by calling NativeMethods.whisper_context_default_params(); to get the default values. I have built the native library on Windows, Mac, and Linux and invoked it in .NET (with and without NativeAOT enabled), and it has worked fine. Nothing is thrown, and I can manipulate the object and send it back though the NativeMethods.

However, when I built Whisper.cpp with Emscripten and ran it in web assembly, any time I called for any struct that included a bool, I would get a System.InvalidProgramException with a blank message.

image

I am unsure how to get better exceptions out of this, this is all I've been able to get. Other functions of accessing the library that don't involve these structs works as expected. Once I saw that it only through this message when I accessed the struct, I changed it to remove the Marshaling and set it to byte

https://github.com/drasticactions/DA.Whisper/blob/wasm-bug/src/DA.Whisper/NativeMethods.g.cs#L367-L378

[StructLayout(LayoutKind.Sequential)]
  public unsafe partial struct whisper_context1
  {
      public byte use_gpu;
      public byte flash_attn;
      public int gpu_device;
      public byte dtw_token_timestamps;
      public whisper_alignment_heads_preset dtw_aheads_preset;
      public int dtw_n_top;
      public whisper_aheads dtw_aheads;
      public nuint dtw_mem_size;
  }

Everything started working! I got the struct, manipulated it, and sent it back through NativeMethods and Whisper loaded it fine.

So I think that means there is a bug with [MarshalAs(UnmanagedType.U1)] and WASM. That should a byte (and indeed, it works on the other platforms I've tried, although maybe that's a mistake too?). I wish to keep my code the same to use the marshal value instead of byte (since it works everywhere else). Is this a bug, or did I make a mistake?

I checked with both net8.0 and net9.0-preview7 and both fail with the same exception.

Reproduction Steps

There are two buttons, one which invokes the Marshaled byte struct, the other which does not. Clicking on the First button should go through right, clicking the second should throw an exception.

Expected behavior

I can access Bool values in Native code via [MarshalAs(UnmanagedType.U1)]

Actual behavior

System.InvalidProgramException thrown.

Regression?

No response

Known Workarounds

Don't use [MarshalAs(UnmanagedType.U1)] but use the literal type byte instead.

Configuration

.NET SDK:
 Version:           9.0.100-preview.7.24407.12
 Commit:            d672b8a045
 Workload version:  9.0.100-manifests.2aef0cee
 MSBuild version:   17.12.0-preview-24374-02+48e81c6f1

ランタイム環境:
 OS Name:     Windows
 OS Version:  10.0.26100
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\9.0.100-preview.7.24407.12\

インストール済みの .NET ワークロード:
新しいマニフェストをインストールするときに loose manifests を使用するように構成されています。
 [android]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    35.0.0-preview.7.41/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.sdk.android\35.0.0-preview.7.41\WorkloadManifest.json
   インストールの種類:              Msi

 [aspire]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    8.2.0/8.0.100
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.aspire\8.2.0\WorkloadManifest.json
   インストールの種類:        FileBased

 [ios]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    17.5.9231-net9-p7/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.sdk.ios\17.5.9231-net9-p7\WorkloadManifest.json
   インストールの種類:              Msi

 [maccatalyst]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    17.5.9231-net9-p7/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.sdk.maccatalyst\17.5.9231-net9-p7\WorkloadManifest.json
   インストールの種類:              Msi

 [maui-windows]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    9.0.0-preview.7.24407.4/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.sdk.maui\9.0.0-preview.7.24407.4\WorkloadManifest.json
   インストールの種類:              Msi

 [wasm-tools]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    9.0.0-preview.7.24405.7/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.workload.mono.toolchain.current\9.0.0-preview.7.24405.7\WorkloadManifest.json
   インストールの種類:              Msi

 [wasm-tools-net8]
   インストール ソース: SDK 9.0.100-preview.7, VS 17.12.35209.166
   マニフェストのバージョン:    9.0.0-preview.7.24405.7/9.0.100-preview.7
   マニフェスト パス:       C:\Program Files\dotnet\sdk-manifests\9.0.100-preview.7\microsoft.net.workload.mono.toolchain.net8\9.0.0-preview.7.24405.7\WorkloadManifest.json
   インストールの種類:              Msi

Host:
  Version:      9.0.0-preview.7.24405.7
  Architecture: x64
  Commit:       static

.NET SDKs installed:
  9.0.100-preview.7.24407.12 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.32 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 9.0.0-preview.7.24406.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.32 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 9.0.0-preview.7.24405.7 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.32 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.7 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 9.0.0-preview.7.24405.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Other information

No response

dotnet-policy-service[bot] commented 1 month ago

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

lambdageek commented 1 month ago

/cc @pavelsavara

pavelsavara commented 1 month ago

cc @kg

kg commented 1 month ago

I believe the root cause here is that bool in .NET is sometimes 4 bytes in size, not 1 byte, so by doing MarshalAs you're requiring pinvoke to make a temporary copy of your struct with a different size and copy all the values into it, performing truncation on the bools as needed. We generally don't support this kind of struct marshaling on WASM, at least not yet.

Also note that bool is not a blittable type according to https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types. Right now you should only expect blittable structs to marshal properly on the wasm target. We may be able to relax these restrictions in the future. These limitations are due to the extreme complexity of p/invoke and the existing implementations not being directly portable to the WASM target.

As a workaround, consider wrapping the bytes with bool properties. That would maintain a blittable struct (with private byte fields) that has a public interface with bools in it.

lambdageek commented 1 month ago

you might be able to set the DisableRuntimeMarshalingAttribute on the assembly to make a struct containing a bool blittable (the MarshalAsAttribute must not be used then - it will use the underlying type (ie byte) as the marshaling type) https://github.com/dotnet/runtime/issues/60639 this is assuming all your pinvokes in the assembly are using the source generator [LibraryImport] mechanism instead of the runtime built-in marshaling