-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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.
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
Labels
Type
Projects
Status