跨语言调用C#代码的新方式-DllExport

简介

上一篇文章使用C#编写一个.NET分析器文章发布以后,很多小伙伴都对最新的NativeAOT函数导出比较感兴趣,今天故写一篇短文来介绍一下如何使用它。

在以前,如果有其他语言需要调用C#编写的库,那基本上只有通过各种RPC的方式(HTTP、GRPC)或者引入一层C++代理层的方式来调用。

自从微软开始积极开发和研究Native AOT以后,我们有了新的方式。那就是直接使用Native AOT函数导出的方式,其它语言(C++、Go、Java各种支持调用导出函数的语言)就可以直接调用C#导出的函数来使用C#库。

废话不多说,让我们开始尝试。

开始尝试

我们先来一个简单的尝试,就是使用C#编写一个用于对两个整数求和的Add方法,然后使用C语言调用它。

1 .首先我们需要创建一个新的类库项目。这个大家都会了,可以直接使用命令行新建,也可以通过VS等IDE工具新建。

dotnetnewclasslib-oCSharpDllExport

2 .为我们的项目加入Native AOT的支持,根据.NET的版本不同有不同的方式。

  • 如果你是.NET6则需要引入 Microsoft.DotNet.ILCompiler 这个Nuget包,需要指定为 7.0.0-preview.7.22375.6 ,新版本的话只允许.NET7以上使用。更多详情请看hez2010的博客 https://www.cnblogs.com/hez2010/p/dotnet-with-native-aot.html
  • 如果是.NET7那么只需要在项目属性中加入 <PublishAot>true</PublishAot> 即可,笔者直接使用的.NET7,所以如下配置就行。

3 .编写一个静态方法,并且为它打上 UnmanagedCallersOnly 特性,告诉编译器我们需要将它作为函数导出,指定名称为Add。

usingSystem.Runtime.InteropServices;
namespaceCSharpDllExport
{
publicclassDoSomethings
{
[UnmanagedCallersOnly(EntryPoint="Add")]
publicstaticintAdd(inta,intb)
{
returna+b;
}
}
}

4 .使用 dotnet publish -p:NativeLib=Shared -r win-x64 -c Release 命令发布共享库。共享库的扩展名在不同的操作系统上不一样,如 .dll .dylib .so 。当然我们也可以发布静态库,只需要修改为 -p:NativeLib=Static 即可。

跨语言调用C#代码的新方式-DllExport

5 .使用 DLL Export Viewer 工具打开生成的 .dll 文件,查看函数导出是否成功,如下图所示,我们成功的把ADD方法导出了,另外那个是默认导出用于Debugger的方法,我们可以忽略。工具*载下**链接放在文末。

跨语言调用C#代码的新方式-DllExport

6 .编写一个C语言项目来测试一下我们的ADD方法是否可用。

#definePathToLibrary"E:\\MyCode\\BlogCodes\\CSharp-Dll-Export\\CSharpDllExport\\CSharpDllExport\\bin\\Release\\net7.0\\win-x64\\publish\\CSharpDllExport.dll"
//导入必要的头文件
#include<windows.h>
#include<stdlib.h>
#include<stdio.h>
intcallAddFunc(char*path,char*funcName,inta,intb);
intmain()
{
//检查文件是否存在
if(access(PathToLibrary,0)==-1)
{
puts("没有在指定的路径找到库文件");
return0;
}
//计算两个值的和
intsum=callAddFunc(PathToLibrary,"Add",2,8);
printf("两个值的和是%d\n",sum);
}
intcallAddFunc(char*path,char*funcName,intfirstInt,intsecondInt)
{
//调用C#共享库的函数来计算两个数的和
HINSTANCEhandle=LoadLibraryA(path);
typedefint(*myFunc)(int,int);
myFuncMyImport=(myFunc)GetProcAddress(handle,funcName);
intresult=MyImport(firstInt,secondInt);
returnresult;
}

7 .跑起来看看

跨语言调用C#代码的新方式-DllExport

这样我们就完成了一个C#函数导出的项目,并且通过C语言调用了C#导出的dll。同样我们可以使用Go的 syscall 、Java的 JNI 、Python的 ctypes 来调用我们生成的dll,在这里就不再演示了。

限制

使用这种方法导出的函数同样有一些限制,以下是在决定导出哪种托管方法时要考虑的一些限制:

  • 导出的方法必须是静态方法。
  • 导出的方法只能接受或返回基元或值类型(即结构体,如果有引用类型,那必须像P/Invoke一样封送所有引用类型参数)。
  • 无法从常规托管C#代码调用导出的方法,必须走Native AOT,否则将引发异常。
  • 导出的方法不能使用常规的C#异常处理,它们应改为返回错误代码。

数据传递引用类型

如果是引用类型的话注意需要传递指针或者序列化以后的结构体数据,比如我们编写一个方法连接两个 string ,那么C#这边就应该这样写:

[UnmanagedCallersOnly(EntryPoint="ConcatString")]
publicstaticIntPtrConcatString(IntPtrfirst,IntPtrsecond)
{
//从指针转换为string
stringmy1String=Marshal.PtrToStringAnsi(first);
stringmy2String=Marshal.PtrToStringAnsi(second);
//连接两个string
stringconcat=my1String+my2String;
//将申请非托管内存string转换为指针
IntPtrconcatPointer=Marshal.StringToHGlobalAnsi(concat);
//返回指针
returnconcatPointer;
}

对应的C代码也应该传递指针,如下所示:

//拼接两个字符串
char*result=callConcatStringFunc(PathToLibrary,"ConcatString",".NET","yyds");
printf("拼接符串的结果为%s\n",result);
....
char*callConcatStringFunc(char*path,char*funcName,char*firstString,char*secondString)
{
HINSTANCEhandle=LoadLibraryA(path);
typedefchar*(*myFunc)(char*,char*);
myFuncMyImport=(myFunc)GetProcAddress(handle,funcName);
//传递指针并且返回指针
char*result=MyImport(firstString,secondString);
returnresult;
}

运行一下,结果如下所示:

跨语言调用C#代码的新方式-DllExport

附录

  • 本文代码链接:https://github.com/InCerryGit/BlogCodes/tree/main/CSharp-Dll-Export
  • DLL Export Viewer*载下**链接:https://www.nirsoft.net/utils/dllexp-x64.zip
  • NativeAOT文档:https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs