[C#]创建附带多平台本机代码的NuGet包

创建附带多平台本机代码的NuGet包

一般只包含托管代码的 .NET工程直接打包就完事了。但是现在有一个 .NET工程包含了平台相关 (P/Invoke) 的调用,需要平台相关的非托管程序集,这个程序集需要手动提供,打包的时候要怎么做?

实际上过程很简单…但是…M$这文档太详细了半天没找着对应位置,在某404搜索引擎上搜了半天才在nuget官方的repo里找到一个issue,里说了正确方法,结合之前搜到的教程终于是搞定了…所以在这里特地记录一下防止遗忘

假设现在我们有个类库工程叫做SuperAdd.NET,它现在长这样:

1
2
3
4
5
6
├───SuperAdd.NET
│ Omg.cs
│ SuperAdd.NET.csproj

└───SuperAdd.NET.Native
SuperAdd.NET.Native.cpp

获取平台的动态链接库

文章重点不在C/C++这边,这里应该简略说的,不过呢为了演示还是写详细点…

文件夹SuperAdd.NET.Native包含C++源码,SpuerAdd.NET.Native.cpp的内容是:

1
2
3
4
5
6
7
8
9
10
11
#ifdef _WIN32
#define EXPORT_API __declspec(dllexport)
#else
#define EXPORT_API __attribute__((visibility("default")))
#endif

#include <cstdint>

extern "C" EXPORT_API std::int32_t SuperAdd(std::int32_t x, std::int32_t y) {
return x + y;
}

显然这份C++源码是可以跨平台的

我们将它分别在win和linux下编译成动态链接库。

现在整个工程长这样:

1
2
3
4
5
6
7
8
├───SuperAdd.NET
│ Omg.cs
│ SuperAdd.NET.csproj

└───SuperAdd.NET.Native
SuperAdd.NET.Native.cpp
SuperAdd.NET.Native.dll
SuperAdd.NET.Native.so

NuGet包结构

M$的文档写的非常详细 https://docs.microsoft.com/zh-cn/nuget/create-packages/creating-a-package

这里总结一下:

  • lib:存托管程序集,它的结构是:lib/{tfm}/*.dll
  • runtimes:存平台相关程序集,它的结构是:runtimes/{rid}/native/*.*

其中,tfm 表示目标框架,就是netstandard2.1之类的名字,rid是运行时标识符,类似win-x64linux-x64之类的名字

所以为了打包,我们需要按照NuGet包的结构编排我们的项目

配置 .NET工程

由于我们跨win和linux,所以根据runtimes的结构,我们需要将动态链接库复制到对应文件夹里

现在的工程结构长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├───SuperAdd.NET
│ │ Omg.cs
│ │ SuperAdd.NET.csproj
│ │
│ └───Externs
│ └───runtimes
│ ├───linux-x64
│ │ └───native
│ │ SuperAdd.NET.Native.so
│ │
│ └───win-x64
│ └───native
│ SuperAdd.NET.Native.dll

└───SuperAdd.NET.Native
SuperAdd.NET.Native.cpp
SuperAdd.NET.Native.dll
SuperAdd.NET.Native.so

其中,Externs这个文件夹的名字是可以随意替换的,替换成啥都可以

接着打开SuperAdd.NET.csproj,我们需要在里面添加一个Target

1
2
3
4
5
<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Include="Externs\runtimes\**\*.dll" Pack="True" PackagePath="runtimes" />
</ItemGroup>
</Target>

其中,第三行Include里面填的就是需要打包的文件,所以这里别忘了把Externs换成前面你的文件夹名字

然后在PropertyGroup属性里添加<GeneratePackageOnBuild>true</GeneratePackageOnBuild>,这样就可以在构建工程时自动打包了,这是个可选项,不过我觉得加上方便些

现在,SuperAdd.NET.csproj长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Include="Externs\runtimes\**\*.*" Pack="True" PackagePath="runtimes" />
</ItemGroup>
</Target>

</Project>

现在构建工程

1
dotnet build --configuration Release

就可以看到输出里的NuGet包了

我们打开看一下

确确实实打包进去了

顺便贴一下Omg.cs的内容:

1
2
3
4
5
6
7
using System.Runtime.InteropServices;

public static class Omg
{
[DllImport("SuperAdd.NET.Native")]
public static extern int SuperAdd(int x, int y);
}

可以看到和C++侧的完全对应

测试

现在再新建个工程,导入我们刚刚生成的包(具体如何导入本地包可以自己搜一下)

直接上结果图吧

Surprise !

我们还可以打开构建目录里的 deps.json 文件

看到targets字段下,我们的SuperAdd.NET里面确实包含了runtimeTargets,这样 .NET就会在运行时根据平台自动帮我们加载对应的动态链接库,不再需要手动编写deps.json了,还是挺方便的


[C#]创建附带多平台本机代码的NuGet包
https://ksgfk.github.io/2022/07/15/CSharp-创建附带多平台本机代码的NuGet包/
作者
ksgfk
发布于
2022年7月15日
许可协议