docs: 更新 Linux 兼容性相关的介绍。
最近 .Net 7 发布之后,因为带了 AOT 编译器,又爆发了一波热度,正好我最近有需求需要使用到这个功能,本文就记录下如何实现将 .Net 7 库编译成静态库,然后用 Rust 链接。
.Net 7
AOT
Rust
本文实现的是将一个非标准的 DES 算法编译成静态库,供 Rust 调用。该 DES 算法的 C# 实现在这里可以找到:https://github.com/fygroup/Security/blob/master/DES.cs。
DES
C#
本文项目的目录结构为:
./call-net-from-rust-statically ├── des-lib │ ├── des-lib.csproj │ └── DES.cs ├── Cargo.toml ├── build.rs └── src └── main.rs
先创建好 call-net-from-rust-statically 目录:
call-net-from-rust-statically
mkdir call-net-from-rust-statically
首先创建项目:
cd call-net-from-rust-statically dotnet new classlib -n des-lib
将 Class1.cs 重命名为 DES.cs,然后把上面链接中的 DES 类复制到 DES.cs 中,改下命名空间,再加上导出函数的代码,如下:
Class1.cs
DES.cs
namespace des_lib; using System.Runtime.InteropServices; public class DES { [UnmanagedCallersOnly(EntryPoint = "wtf_des_encrypt")] public static nint FFI_Encrypt(nint message, nint key) { var managedMessage = Marshal.PtrToStringUTF8(message); var managedKey = Marshal.PtrToStringUTF8(key); if (managedKey == null || managedMessage == null) { return nint.Zero; } var cipherText = EncryptDES(managedMessage, managedKey); return Marshal.StringToHGlobalAnsi(cipherText); } [UnmanagedCallersOnly(EntryPoint = "wtf_des_decrypt")] public static nint FFI_Decrypt(nint cipherMessage, nint key) { var managedCipherMessage = Marshal.PtrToStringUTF8(cipherMessage); var managedKey = Marshal.PtrToStringUTF8(key); if (managedKey == null || managedCipherMessage == null) { return nint.Zero; } var plainText = DecryptDES(managedCipherMessage, managedKey); return Marshal.StringToHGlobalAnsi(plainText); } [UnmanagedCallersOnly(EntryPoint = "wtf_des_free")] public static void FFI_FreeMemory(nint buffer) { Marshal.FreeHGlobal(buffer); } // 将原有 DES 类的内容放在这里。 }
其中 wtf_des_encrypt、wtf_des_decrypt 和 wtf_des_free 就是导出的加密、解密以及释放内存的方法。
wtf_des_encrypt
wtf_des_decrypt
wtf_des_free
配置项目的属性:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <NativeLib>Static</NativeLib> <PublishAot>true</PublishAot> <StripSymbols>true</StripSymbols> <SelfContained>true</SelfContained> </PropertyGroup> </Project>
然后就可以用如下命令编译一下试试看:
cd des-lib dotnet publish -r win-x64 -c Release
在构建完毕之后,会在 bin\Release\net7.0\win-x64\publish 目录下生成 des-lib.lib 文件。
bin\Release\net7.0\win-x64\publish
des-lib.lib
在上面的项目构建成功后,将会把 ilcompiler 包缓存,并可以在该目录 %USERPROFILE%/.nuget/packages/runtime.win-x64.microsoft.dotnet.ilcompiler/7.0.1/sdk 找到链接依赖的一些静态库(注意,版本号可能会变更)。
ilcompiler
%USERPROFILE%/.nuget/packages/runtime.win-x64.microsoft.dotnet.ilcompiler/7.0.1/sdk
在 call-net-from-rust-statically 目录中创建 Rust 项目:
cd call-net-from-rust-statically cargo init
先添加 windows 依赖,这是因为在链接的时候,.Net 运行时会依赖 Win32 API:
windows
.Net
Win32 API
cargo add windows
添加 build.rs,一定要注意修改 sdk_path 中的 ilcompiler 版本号(本文讲的是实现步骤,最终的代码我会把 des-lib 的构建也放在 build.rs 中,并从构建的输出中寻找这个版本号,而不需要写死):
build.rs
sdk_path
des-lib
use std::path::PathBuf; fn main() { let user_profile: PathBuf = std::env::var("USERPROFILE").unwrap().into(); let sdk_path: PathBuf = (user_profile) .join(".nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\7.0.1\sdk"); let manifest_dir: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into(); let des_lib_path = manifest_dir.join("des-lib"); println!("cargo:rustc-link-arg=/INCLUDE:NativeAOT_StaticInitialization"); println!("cargo:rustc-link-search={}", sdk_path.display()); println!( "cargo:rustc-link-search={}\bin\Release\net7.0\win-x64\publish", des_lib_path.display() ); // 新版本的 windows crate 已不再提供 windows.lib,此处不再需要。 // println!("cargo:rustc-link-lib=static=windows"); println!("cargo:rustc-link-lib=static=bootstrapperdll"); println!("cargo:rustc-link-lib=static=Runtime.WorkstationGC"); println!("cargo:rustc-link-lib=static=System.Globalization.Native.Aot"); println!("cargo:rustc-link-lib=static=des-lib"); }
接下来就是调用了,在 main.rs 中添加:
main.rs
// 链接 windows crate。 extern crate windows; extern "C" { fn wtf_des_encrypt(message: *const u8, key: *const u8) -> *const u8; fn wtf_des_decrypt(cipher_text: *const u8, key: *const u8) -> *const u8; fn wtf_des_free(ptr: *const u8); } fn main() { let key = b"keyrust // 链接 windows crate。 extern crate windows; extern "C" { fn wtf_des_encrypt(message: *const u8, key: *const u8) -> *const u8; fn wtf_des_decrypt(cipher_text: *const u8, key: *const u8) -> *const u8; fn wtf_des_free(ptr: *const u8); } fn main() { let key = b"key\0"; let cipher_text = unsafe { wtf_des_encrypt(b"message\0".as_ptr(), key.as_ptr()) }; let cipher_text = unsafe { std::ffi::CStr::from_ptr(cipher_text as *const i8) }; let plain_text = unsafe { wtf_des_decrypt(cipher_text.as_ptr() as _, key.as_ptr()) }; let plain_text = unsafe { std::ffi::CStr::from_ptr(plain_text as *const i8) }; println!("cipher_text: {}", cipher_text.to_str().unwrap()); println!("plain_text: {}", plain_text.to_str().unwrap()); unsafe { wtf_des_free(cipher_text.as_ptr() as _); wtf_des_free(plain_text.as_ptr() as _); } } "; let cipher_text = unsafe { wtf_des_encrypt(b"messagerust // 链接 windows crate。 extern crate windows; extern "C" { fn wtf_des_encrypt(message: *const u8, key: *const u8) -> *const u8; fn wtf_des_decrypt(cipher_text: *const u8, key: *const u8) -> *const u8; fn wtf_des_free(ptr: *const u8); } fn main() { let key = b"key\0"; let cipher_text = unsafe { wtf_des_encrypt(b"message\0".as_ptr(), key.as_ptr()) }; let cipher_text = unsafe { std::ffi::CStr::from_ptr(cipher_text as *const i8) }; let plain_text = unsafe { wtf_des_decrypt(cipher_text.as_ptr() as _, key.as_ptr()) }; let plain_text = unsafe { std::ffi::CStr::from_ptr(plain_text as *const i8) }; println!("cipher_text: {}", cipher_text.to_str().unwrap()); println!("plain_text: {}", plain_text.to_str().unwrap()); unsafe { wtf_des_free(cipher_text.as_ptr() as _); wtf_des_free(plain_text.as_ptr() as _); } } ".as_ptr(), key.as_ptr()) }; let cipher_text = unsafe { std::ffi::CStr::from_ptr(cipher_text as *const i8) }; let plain_text = unsafe { wtf_des_decrypt(cipher_text.as_ptr() as _, key.as_ptr()) }; let plain_text = unsafe { std::ffi::CStr::from_ptr(plain_text as *const i8) }; println!("cipher_text: {}", cipher_text.to_str().unwrap()); println!("plain_text: {}", plain_text.to_str().unwrap()); unsafe { wtf_des_free(cipher_text.as_ptr() as _); wtf_des_free(plain_text.as_ptr() as _); } }
Linux 系统上,需要链接的库有些许不一样,具体参见:https://github.com/hamflx/call-net-from-rust-statically/commit/6cb7e9adb0a8faa48afc27e95267163131ca0717 和 https://github.com/hamflx/call-net-from-rust-statically/commit/95d155a309ff5d47b5800fbf8551e3343f3302b0。
如果你加了额外的功能导致构建失败,可以自行查找所依赖的库,并链接。
例如,构建时报错,undefined reference to ``RhRegisterOSModule',我们可以运行如下的代码找到所需的依赖:
undefined reference to ``RhRegisterOSModule'
# 这个路径按你的需要更改。 cd /home/hamflx/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.10 find . -name '*.a' | xargs -I{} nm -o '{}' | grep RhRegisterOSModule
此时,输出结果大致如下:
./framework/libSystem.Native.a:pal_threading.c.o:0000000000000100 T SystemNative_LowLevelMonitor_Release
因此,我们可以链接 System.Native 库。
System.Native
注意,有时输出结果可能有多个,我们需要的是具有 T 标志的库。
T
注意,Linux 系统下,库的顺序会影响符号的解析,有时报错 undefined reference 可能调整下顺序即可。
undefined reference
仓库地址:https://github.com/hamflx/call-net-from-rust-statically,在本文的基础增加了自动构建 C# 项目,自动查找 ilcompiler 的路径并链接。
©Copyright 2023 CCF 开源发展委员会 Powered by Trustie& IntelliDE 京ICP备13000930号
静态链接 C# 到 Rust
最近
.Net 7
发布之后,因为带了AOT
编译器,又爆发了一波热度,正好我最近有需求需要使用到这个功能,本文就记录下如何实现将.Net 7
库编译成静态库,然后用Rust
链接。本文实现的是将一个非标准的
DES
算法编译成静态库,供Rust
调用。该DES
算法的C#
实现在这里可以找到:https://github.com/fygroup/Security/blob/master/DES.cs。本文项目的目录结构为:
先创建好
call-net-from-rust-statically
目录:C# 项目部分
首先创建项目:
将
Class1.cs
重命名为DES.cs
,然后把上面链接中的DES
类复制到DES.cs
中,改下命名空间,再加上导出函数的代码,如下:其中
wtf_des_encrypt
、wtf_des_decrypt
和wtf_des_free
就是导出的加密、解密以及释放内存的方法。配置项目的属性:
然后就可以用如下命令编译一下试试看:
在构建完毕之后,会在
bin\Release\net7.0\win-x64\publish
目录下生成des-lib.lib
文件。Rust 项目部分
在上面的项目构建成功后,将会把
ilcompiler
包缓存,并可以在该目录%USERPROFILE%/.nuget/packages/runtime.win-x64.microsoft.dotnet.ilcompiler/7.0.1/sdk
找到链接依赖的一些静态库(注意,版本号可能会变更)。在
call-net-from-rust-statically
目录中创建Rust
项目:先添加
windows
依赖,这是因为在链接的时候,.Net
运行时会依赖Win32 API
:添加
build.rs
,一定要注意修改sdk_path
中的ilcompiler
版本号(本文讲的是实现步骤,最终的代码我会把des-lib
的构建也放在build.rs
中,并从构建的输出中寻找这个版本号,而不需要写死):接下来就是调用了,在
main.rs
中添加:Linux 兼容
Linux 系统上,需要链接的库有些许不一样,具体参见:https://github.com/hamflx/call-net-from-rust-statically/commit/6cb7e9adb0a8faa48afc27e95267163131ca0717 和 https://github.com/hamflx/call-net-from-rust-statically/commit/95d155a309ff5d47b5800fbf8551e3343f3302b0。
如果你加了额外的功能导致构建失败,可以自行查找所依赖的库,并链接。
例如,构建时报错,
undefined reference to ``RhRegisterOSModule'
,我们可以运行如下的代码找到所需的依赖:此时,输出结果大致如下:
因此,我们可以链接
System.Native
库。注意,有时输出结果可能有多个,我们需要的是具有
T
标志的库。注意,Linux 系统下,库的顺序会影响符号的解析,有时报错
undefined reference
可能调整下顺序即可。最终版本
仓库地址:https://github.com/hamflx/call-net-from-rust-statically,在本文的基础增加了自动构建
C#
项目,自动查找ilcompiler
的路径并链接。