# 写在前面

  • 在.Net FrameWork 时代,本人常用的导出函数的方式为 DLLExport,但是.Net 技术发展太快,项目作者没有继续跟进高版本的.Net 版本,最高支持到似乎是.NetCore2.1,对于这个版本以下的项目使用这个项目完全足够,不过只限于 Windows 平台
  • DNNE 使用方法对于 DLLExport 更复杂一些,不过高版本也没得选。DNNE 提供跨平台编译,不只限于 Windows 平台,且要求.Net6 以上

# 函数导出表

PE 文件有导入表与导出表,导入表是引入其他 PE 文件的函数,导出表是本文件向其他文件提供的函数入口表。对于大部分的查壳工具或是 PE 文件工具都提供了导出表以及导入表的查看。
导出表

旧酷 Q 插件即使从 cpk 文件解包出来也无法直接使用的原因就是,酷 Q 在打包插件的时候就将导出表抹除了,所以在兼容框架初期,有人手动从 DLL 文件手动找函数入口,补全了导出表,并使插件工作正常。(我见过的是回溯的骰娘,找了找没找到文件,展示不了力)

# DLLExport

# 安装方式

# Nuget

  • 在 Nuget 包管理器内直接搜索 DllExport
  • Install-Package DllExport

在弹出的窗口勾选需要进行函数导出的项目,选择右上角的 Apply 即可
file

# .bat

(我上传键呢)
放在项目根目录之后打开即可,在下载自己的依赖之后,会打开上一步弹出的窗口,相同操作

# 函数导出

# 最小示例

1
2
3
4
5
[DllExport]
public static int test(int x, int y)
{
return x + y;
}

编译之后就有了名为 test 的导出函数

# 规定名称

1
2
3
4
5
[DllExport(ExportName = "TestFunction")]
public static int test(int x, int y)
{
return x + y;
}

这样就有了名为 TestFunction 的导出函数

# 复杂对象

参数以及返回值都需要使用 struct 而不应该使用 class

# string

# 简易

1
2
IntPtr intPtr = Marshal.StringToHGlobalAnsi(str);
string str = Marshal.PtrToStringAnsi(intPtr);

使用系统默认编码,编码格式受限

# 编码 (GB18030 示例)

1
2
3
4
5
6
7
Encoding GB18030 = Encoding.GetEncoding("GB18030");
var b = Encoding.UTF8.GetBytes(msg);
msg = GB18030.GetString(Encoding.Convert(Encoding.UTF8, GB18030, b));
byte[] messageBytes = GB18030.GetBytes(msg + "\0");
var messageIntptr = Marshal.AllocHGlobal(messageBytes.Length);
Marshal.Copy(messageBytes, 0, messageIntptr, messageBytes.Length);
return messageIntptr;

# 解码(GB18030 示例)

1
2
3
4
5
6
7
8
9
10
11
12
[DllImport("kernel32.dll", EntryPoint = "lstrlenA", CharSet = CharSet.Ansi)]
public extern static int LstrlenA(IntPtr ptr);

Encoding GB18030 = Encoding.GetEncoding("GB18030");
int len = LstrlenA(strPtr);
if (len == 0)
{
return string.Empty;
}
byte[] buffer = new byte[len];
Marshal.Copy(strPtr, buffer, 0, len);
return GB18030.GetString(buffer);

# 实用部分

# 输出导出表

.\DllExport.bat -pe-exp-list

1
2
3
4
5
6
7
PS E:\DO\ClassLibrary1\ClassLibrary2\bin\Debug\net7.0> .\DllExport.bat -pe-exp-list ClassLibrary2NE.dll
TestFunction
default_dnne_abort
get_hostfxr_path
preload_runtime
set_failure_callback
try_preload_runtime

# 命令行代替窗口

这个可能更适用于类似 GitHub Action 的自动化操作
.\DllExport -action Restore

# DNNE

# 依赖

https://github.com/AaronRobinsonMSFT/DNNE#dnne-nupkg-requirements

# 安装

  • 在 Nuget 包管理器内直接搜索 DNNE
  • Install-Package DNNE

# 代码

# 最小示例

1
2
3
4
5
[UnmanagedCallersOnly]
public static int test(int x, int y)
{
return x + y;
}

这样便导出名为 test 的函数

# 规定名称

1
2
3
4
5
[UnmanagedCallersOnly(EntryPoint = "TestFunction")]
public static int test(int x, int y)
{
return x + y;
}

这样就有了名为 TestFunction 的导出函数

# x86

AnyCPU 与 x86 导出上略有不同

1
2
3
4
5
[UnmanagedCallersOnly(EntryPoint = "TestFunction", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static int test(int x, int y)
{
return x + y;
}

之后…… 编译就失败了

1
2
3
4
1>  正在创建库 E:\DO\ClassLibrary1\ClassLibrary2\obj\x86\Debug\net7.0\dnne\bin\ClassLibrary2NE.lib 和对象 E:\DO\ClassLibrary1\ClassLibrary2\obj\x86\Debug\net7.0\dnne\bin\ClassLibrary2NE.exp
1>platform.obj : error LNK2019: 无法解析的外部符号 _get_hostfxr_path@12,函数 _load_hostfxr 中引用了该符号
1>C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\7.0.7\runtimes\win-x64\native\libnethost.lib : warning LNK4272: 库计算机类型“x64”与目标计算机类型“x86”冲突
1>E:\DO\ClassLibrary1\ClassLibrary2\obj\x86\Debug\net7.0\dnne\bin\ClassLibrary2NE.dll : fatal error LNK1120: 1 个无法解析的外部命令

解决方案就是规定生成平台为 win-x86

  1. 打开项目 .csproj 文件
  2. PropertyGroup 添加一项 <RuntimeIdentifier>win-x86</RuntimeIdentifier>
    file

# 复杂对象

参数以及返回值都需要使用 struct 而不应该使用 class

# string

DNNE 也不支持直接 string 返回,查阅上一章的 string 部分

# .Net FrameWork 兼容?

兼容不好,由于二者使用的部分项目集名称相同但版本号不同,使用.Net7 程序可以正常加载.netFramework 文件,但是调用时就会发生异常,不过有时候也没事,就体验来说是可能反射时发生的异常。
若想彻底解决问题,就需要把.netFramework 程序改为.netStandard 编译(咋可能

# 部署

此文件生成出来是不能只把 dll 拿走就用的,需要连着原来的 dll 以及运行 json 一起拿走
file
CQPNet7.dll 以及 CQPNet7.runtimeconfig.json 都是原项目生成的,这三个文件需要在一起才能正常运作

# 生成改名

生成出来的文件是有 NE 后缀的,这个可以配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>CQP</RootNamespace>
<Platforms>AnyCPU;x86</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<EnableDynamicLoading>true</EnableDynamicLoading>
<DnneNativeBinaryName>CQP</DnneNativeBinaryName>
<!-- 此处为生成的名称 -->
<DnneNativeBinarySuffix></DnneNativeBinarySuffix>
<!-- 此处为生成的后缀 -->
</PropertyGroup>

这样改之后,生成的文件就是 CQP.dll