Skip to content

Strange issues when mixing [UnmanagedCallersOnly] with [DLLImport] - "Method's type signature is not PInvoke compatible" emitted for incorrect methods #92409

@kkukshtel

Description

@kkukshtel

Description

I'm working with some bindings for sokol and am trying to bind a function callback. There is a logger on the struct I'm binding that has the following signature:

public unsafe partial struct sapp_logger {
    public delegate* unmanaged[Cdecl]<sbyte*, uint, uint, sbyte*, uint, sbyte*, void*, void> func;
    public void* user_data;
}

The library provides a default logger to attach that is also in the Pinvoke code with the following signature:

[DllImport("libs/sokol", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static extern void slog_func([NativeTypeName("const char *")] sbyte* tag, [NativeTypeName("uint32_t")] uint log_level, [NativeTypeName("uint32_t")] uint log_item, [NativeTypeName("const char *")] sbyte* message, [NativeTypeName("uint32_t")] uint line_nr, [NativeTypeName("const char *")] sbyte* filename, void* user_data);

I'm binding the function as such here:

static internal void Boot() {
  unsafe
  {
      byte[] bytes = System.Text.Encoding.UTF8.GetBytes("my window");
      fixed (byte* ptr = bytes)
      {
          //init
          sapp_desc desc = default;
          desc.width = 1920;
          desc.height = 1080;
          desc.icon.sokol_default = 1;
          desc.window_title = (sbyte*)ptr;
          desc.init_cb = &Initialize;
          desc.event_cb = &Event;
          desc.frame_cb = &Frame;
          desc.cleanup_cb = &Cleanup;
          desc.logger.func = &Log.slog_func; //<----------------- here
          App.run(&desc);
      }
  }
}

Worth noting that Event/Frame/Cleanup are defined like this:

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static unsafe void Event(sapp_event* e) {}

When I run the code, the first time the native internal binding code encounters some error/warning and attempts to use the bound logger, the program fails and throws:
Unhandled exception. System.NotSupportedException: Method's type signature is not PInvoke compatible.

However, the error is thrown for the entry function into the native layer, at any point where the error may occur, even if the actual PInvoke signature is correct.

The "erroring" function call was:
Gfx.make_pipeline(&pipeline_desc);
Bound as:

[DllImport("libs/sokol", CallingConvention = CallingConvention.Cdecl, EntryPoint = "sg_make_pipeline", ExactSpelling = true)]
public static extern sg_pipeline make_pipeline([NativeTypeName("const sg_pipeline_desc *")] sg_pipeline_desc* desc);

From the C header:

sg_pipeline sg_make_pipeline(const sg_pipeline_desc* desc);

And notably, other methods that follow this same header/binding convention do not throw this same signature error:

[DllImport("libs/sokol", CallingConvention = CallingConvention.Cdecl, EntryPoint = "sg_make_image", ExactSpelling = true)]
public static extern sg_image make_image([NativeTypeName("const sg_image_desc *")] sg_image_desc* desc);

[DllImport("libs/sokol", CallingConvention = CallingConvention.Cdecl, EntryPoint = "sg_make_sampler", ExactSpelling = true)]
public static extern sg_sampler make_sampler([NativeTypeName("const sg_sampler_desc *")] sg_sampler_desc* desc);

I spent forever trying to debug the pipeline struct, but realized over time that the function/struct were totally fine. It was an issue with internal calls throwing their internal errors, that reported up to the logger.

Rewrapping the logger as:

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
static unsafe void Logger(sbyte* tag, uint log_level, uint log_item, sbyte* message, uint line_nr, sbyte* filename, void* user_data)  {
    delegate* unmanaged[Cdecl]<sbyte*, uint, uint, sbyte*, uint, sbyte*, void*, void> ptr = &Log.slog_func;
    ptr(tag, log_level, log_item, message, line_nr, filename, user_data);
}

Will throw the same error at the ptr call. (Thanks @mmozeiko).

It seems like it is a bad idea to try and mix UnmanagedCallersOnly and DllImport maybe? I'm not aware of any documentation as such. But if there's any request here it's for a more elucidating error message to be reported when this problem is encountered, as I went on a wild goose chase for a long time trying to debug unrelated parts of the PInvoke code because it made it sound like the signature was incorrect.

Weirdly enough, you can "fix" the issue by binding the logger like this instead:

desc.logger.func = (delegate* unmanaged[Cdecl]<sbyte*, uint, uint, sbyte*, uint, sbyte*, void*, void>)NativeLibrary.GetExport(NativeLibrary.Load("sokol"), "slog_func");

Reproduction Steps

The best way to debug this would be to use a native library that calls some function internally that can be "improperly bound` in the managed layer. I'm attaching a zip here of the whole project (it's pretty small). This is a library, so if you just build this and bind to some Program.cs that calls Engine.Init() you'll reproduce the issue.

Dinghy.Core.zip

Expected behavior

I would have expected the binding to work as is, but if not that I would have expected a better error message to tell me what was actually failing, not that the Pinvoke signature for an unrelated method was incorrect.

Actual behavior

When run, the program reports that the method signature for the entry point function in the manged layer has the wrong Pinvoke signature, which is incorrect. The signature is right.

Regression?

No response

Known Workarounds

Binding directly as:

`desc.logger.func = (delegate* unmanaged[Cdecl]<sbyte*, uint, uint, sbyte*, uint, sbyte*, void*, void>)NativeLibrary.GetExport(NativeLibrary.Load("sokol"), "slog_func");`

For the logger makes the program work instead of using extern Pinvoke code.

Configuration

Windows 64bit, .NET 7.0

Other information

Bindings are generated via https://github.com/dotnet/ClangSharp cc @tannergooding

I think the bindings are correct here, it's more that I'm unclear the best way to bind delegate* unmanaged[Cdecl]<> to an extern func tagged with [UnmanagedCallersOnly] (and if you can even do that).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-Interop-coreclrin-prThere is an active PR which will close this issue when it is merged

    Type

    No type

    Projects

    Status

    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions