Blazor WASM中能否通过反向P/Invoke从C/C++调用C#方法?
Blazor WASM 原生代码反向P/Invoke问题
项目背景
- 仅具备C/C++编程基础,观看Steve Sanderson与Matthew Leibowitz在ASP.NET Community Standup的两次技术分享后,计划搭建实验项目验证相关技术概念
- 项目目标:将字节数组格式的视频文件传入原生代码,在原生层完成所有视频帧提取后,将结果回传至C#层,供SkiaSharp画布渲染使用
- 当前进展:已经实现字节数组向原生代码的传递,但无法完成原生C代码对C#方法的调用,尝试反向P/Invoke方案时出现运行时断言错误
现有实现代码
原生C/C++侧代码
测试用回调代码:
extern "C" { void test_class_callmycallback( void callback(int,int)) { callback(3,5); } }
实际业务逻辑需要实现的效果:
void test_class_renderpicture(void *obj, void (*func)(uint8_t *, int)) { ((TestClass *)obj)->getBytesWithCallback(func); } }
C#侧代码
[DllImport("Test")] static extern void test_class_callmycallback([MarshalAs(UnmanagedType.FunctionPtr)] CallbackDelegate func); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void CallbackDelegate(int arg1, int arg2); public void RenderPicture() { var managedDel = new CallbackDelegate(Multiply); test_class_callmycallback(Multiply); //RenderPictureDelegate managedDelegate = new RenderPictureDelegate(RenderPictureCallback); //test_class_renderpicture(this.handle, managedDelegate); } public void Multiply(int a,int b) { var c=a * b; }
编译与引入配置
- 原生代码编译命令:
emcc myfile.cpp -shared -o Test.o - Blazor项目引入配置:
<ItemGroup> <NativeFileReference Include="Test.o" /> </ItemGroup>
运行时错误
项目整体编译无异常,运行时抛出Mono loader.c断言错误,错误栈如下:
Assertion at /__w/1/s/src/mono/mono/metadata/loader.c:1806, condition
`' not met 573846 @ dotnet..bnoeqi2kuy.js:1430
_emscripten_asm_const_int @ dotnet..bnoeqi2kuy.js:5673 $wasm_trace_logger @ 02e96b9a:0x228b7a
$eglib_log_adapter @ 02e96b9a:0xc102f
$monoeg_g_logstr @ 02e96b9a:0x20754f
$monoeg_g_logv_nofree @ 02e96b9a:0x2074f3
$mono_assertion_message @ 02e96b9a:0x2075d0
$mono_assertion_message @ 02e96b9a:0x207619
待解决问题
- Blazor WASM项目通过NativeFileReference引入emscripten编译的原生文件时,是否支持从C/C++原生代码反向P/Invoke调用C#方法?如果支持具体实现方式是什么?
- 该场景下
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]与[MarshalAs(UnmanagedType.FunctionPtr)]特性是否为必需配置?emscripten使用__stdcall或__declspec(dllexport)时会抛出警告,与传统本地DllImport场景的配置逻辑是否存在差异?
问题解答
关于反向P/Invoke的支持性与实现方式
Blazor WASM完全支持从emscripten编译的原生代码反向调用C#委托,你遇到的断言错误核心原因有两个:
- 传入委托时没有保持委托的根引用,GC在原生代码触发回调前就回收了委托对象,导致原生侧拿到的是野指针
- emscripten编译WASM目标时参数使用错误,默认不支持裸函数指针跨边界传递,不能用
-shared参数编译供Blazor NativeFileReference引用的目标文件。
正确实现步骤如下:
- 修正emscripten编译命令,去掉
-shared参数,添加导出符号配置和WASM函数表支持参数,正确命令参考:
其中emcc myfile.cpp -o Test.o -sEXPORTED_FUNCTIONS=_test_class_callmycallback,_test_class_renderpicture -sALLOW_TABLE_GROWTH-sALLOW_TABLE_GROWTH是必须添加的参数,反向P/Invoke需要动态往WASM函数表插入委托对应的thunk函数,不开这个参数会直接导致函数表操作失败触发断言。 - C#侧需要把传入原生层的委托存为类级别的字段,保证回调触发前委托不会被GC回收,不要像现有代码那样只在方法内声明临时变量。参考修正后的C#代码:
// 把委托存为实例字段,保持根引用避免被GC回收 private CallbackDelegate _callbackInstance; [DllImport("Test")] static extern void test_class_callmycallback(CallbackDelegate func); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void CallbackDelegate(int arg1, int arg2); public void RenderPicture() { _callbackInstance = Multiply; test_class_callmycallback(_callbackInstance); } public void Multiply(int a,int b) { var c = a * b; } - 后续实现帧数据回传时,原生侧的
uint8_t*指针在C#侧对应IntPtr,不要直接映射为byte[],需要在回调方法里用Marshal.Copy把数据从非托管内存拷贝到托管字节数组中,避免内存访问违规。
关于特性配置与调用约定的差异
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]是必须配置的,WASM平台原生代码默认全部使用cdecl调用约定,不存在stdcall的概念,这也是加__stdcall会出警告的核心原因——WASM没有x86平台上的多种调用约定区分,所有原生函数统一用cdecl规则。[MarshalAs(UnmanagedType.FunctionPtr)]在WASM场景下不是必需配置,委托类型默认就会被封送为函数指针,加了不会出错但属于冗余代码。__declspec(dllexport)在emscripten编译WASM目标时不需要加,emscripten是通过编译参数里的EXPORTED_FUNCTIONS列表控制导出符号的,加这个修饰符没有实际作用还会触发警告,和Windows本地DLL的导出逻辑完全不同。- 传统桌面DllImport场景下需要考虑不同调用约定的栈平衡、符号修饰名差异,在Blazor WASM场景下这些问题都不存在,所有原生函数统一用cdecl,符号名就是C函数名加下划线前缀,不需要额外配置EntryPoint。
内容的提问来源于stack exchange,提问作者Can Günaydın
相关产品推荐
相关产品推荐

