使用 Dotnet 8 AOT 生成 DLL,并被 Java 调用
前言:最近项目中引入了一个加密 DLL 文件,通过 JNA 调用 DLL 对敏感字段进行加密保存。该 DLL 为 Rust 编写,且 Win/Linux/MAC 分别编译出不同的文件 xxx.dll/xxx.so/xxx.dylib,因此突发奇想使用 C# 编写一个 DLL 类库。
使用 Dotnet 8 AOT 生成 DLL,并被 Java 调用
C# 项目
创建 Dotnet 类库项目,并添加BouncyCastle.Cryptography
依赖,并编写加解密方法:
引用内容:
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Paddings;
using Org.BouncyCastle.Crypto.Parameters;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
SM4 加解密方法:
// SM4 Key
private static readonly byte[] key =
[
// Generate your key
];
private static byte[] Sm4EcbEncrypt(byte[] plaintext)
{
var engine = new SM4Engine();
var cipher = new PaddedBufferedBlockCipher(engine, new Pkcs7Padding());
cipher.Init(true, new KeyParameter(key));
return cipher.DoFinal(plaintext);
}
private static byte[] Sm4EcbDecrypt(byte[] ciphertext)
{
var engine = new SM4Engine();
var cipher = new PaddedBufferedBlockCipher(engine, new Pkcs7Padding());
cipher.Init(false, new KeyParameter(key));
return cipher.DoFinal(ciphertext);
}
然后添加导出的函数:
[UnmanagedCallersOnly(EntryPoint = "encrypt_string", CallConvs = [typeof(CallConvCdecl)])]
public static IntPtr EncryptString(IntPtr plaintextPtr)
{
try
{
String? plaintextOptional = Marshal.PtrToStringUni(plaintextPtr);
if (plaintextOptional is null)
{
return IntPtr.Zero;
}
string plaintext = plaintextOptional;
byte[] plainBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] cipherBytes = Sm4EcbEncrypt(plainBytes);
string base64 = Convert.ToBase64String(cipherBytes);
return Marshal.StringToHGlobalUni(base64);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
return IntPtr.Zero;
}
}
[UnmanagedCallersOnly(EntryPoint = "decrypt_string", CallConvs = [typeof(CallConvCdecl)])]
public static IntPtr DecryptString(IntPtr ciphertextPtr)
{
try
{
string? ciphertextOptional = Marshal.PtrToStringUni(ciphertextPtr);
if (ciphertextOptional is null)
{
return IntPtr.Zero;
}
string ciphertextBase64 = ciphertextOptional;
byte[] cipherBytes = Convert.FromBase64String(ciphertextBase64);
byte[] plainBytes = Sm4EcbDecrypt(cipherBytes);
string plaintext = Encoding.UTF8.GetString(plainBytes);
return Marshal.StringToHGlobalUni(plaintext);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
return IntPtr.Zero;
}
}
[UnmanagedCallersOnly(EntryPoint = "free", CallConvs = [typeof(CallConvCdecl)])]
public static void Free(IntPtr ptr)
{
if (ptr != IntPtr.Zero)
{
try
{
Marshal.FreeHGlobal(ptr);
}
catch { }
}
}
注意如果是基本类型可以直接传,如果是 string 这种则需要统一编码,Marshal.PtrToString
有数种方式,为方便统一可以使用UTF-16
格式。
因为返回的类型,也就是Marshal.StringToHGlobalXXX
方法只能用ANSI
或者Uni
(也就是UTF-16
)方式。
然后项目中添加 AOT 设置:
<PropertyGroup>
<PublishAOT>true</PublishAOT>
</PropertyGroup>
然后发布即可(注意,AOT 需要发布,运行时生成的 DLL 不是 AOT)。
Java 侧调用
maven 中添加如下依赖:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.14.0</version>
</dependency>
将生成的 DLL 放在项目根目录或者resource
目录下,创建对应的接口文件:
import com.sun.jna.*;
import java.util.Collections;
public interface SharpLibrary extends Library {
// 这里是 DLL 的名称,不带后缀
SharpLibrary INSTANCE = Native.load("Sharp", SharpLibrary.class,
Collections.singletonMap(Library.OPTION_CALLING_CONVENTION, Function.C_CONVENTION));
// 注意这里用 Pointer,JNA 不会帮你自动释放内存
// 并且由于入参用的是 UTF-16 格式,因此使用 WString 类
Pointer encrypt_string(WString plaintext);
Pointer decrypt_string(WString ciphertext);
void free(Pointer ptr);
}
调用测试:
SharpLibrary lib = SharpLibrary.INSTANCE;
// 调用加密,传入UTF-16字符串,返回Pointer
Pointer pEnc = lib.encrypt_string(new WString("中文测试😊"));
String encrypted = pEnc.getWideString(0); // 读取UTF-16字符串
System.out.println("Encrypted: " + encrypted);
// 调用解密,传入加密后的UTF-16字符串
Pointer pDec = lib.decrypt_string(new WString(encrypted));
String decrypted = pDec.getWideString(0);
System.out.println("Decrypted: " + decrypted);
// 释放内存
lib.free(pEnc);
lib.free(pDec);
更新:经测试,也可以在 C# 侧输入输出都是用 UTF-8 格式,这样 Java 侧就不用使用 WString 了。
C# 侧输入:
Marshal.PtrToStringUTF8(IntPrt textPtr)
C# 侧返回:
Marshal.StringToCoTaskMemUTF8(String text)
C# 侧内存释放:
Marshal.FreeCoTaskMem(IntPtr ptr);