探索 FFI - Rust 与 C# 互调实战
所谓幸福,就是把灵魂安放在适当的位置。
—— 亚里士多德 Aristotle
一、Rust + C# = ?
1、C# 的优势
- 丰富的生态系统:C# 是由微软开发和维护的,拥有强大的 .NET 框架支持,提供了大量的库和工具,可以极大地提高开发效率。Visual Studio 是一个功能强大的集成开发环境(IDE),为 C# 开发提供了卓越的支持。
- 跨平台能力:随着 .NET Core 的推出,C# 现在可以在 Windows、Linux 和 macOS上运行,实现了真正的跨平台开发。
- 面向对象编程:C# 是一种纯粹的面向对象编程语言,支持类、继承、多态等特性,使得代码结构清晰,易于维护和扩展。
- 企业级应用开发:C# 广泛应用于企业级应用开发,特别是在 Web 应用、桌面应用和云服务方面,如 ASP.NET 用于构建高性能的 Web 应用和 API。
- 社区和支持:拥有庞大的开发者社区和丰富的在线资源,包括文档、教程和论坛,帮助开发者快速解决问题。
2、Rust 的优势
- 内存安全:Rust最显著的特点是其内存安全性,通过所有权系统和借用检查器,在编译时防止数据竞争、空指针引用和缓冲区溢出等常见的内存错误。
- 高性能:Rust 的性能接近 C 和 C++,因为它是编译型语言,并且没有运行时开销,非常适合系统编程和性能关键的应用。
- 并发编程:Rust 提供了强大的并发编程模型,通过所有权和类型系统来确保线程安全,避免了传统多线程编程中的许多陷阱。
- 现代化设计:Rust 结合了现代编程语言的许多优点,如模式匹配、闭包、泛型和函数式编程风格,使得代码更加简洁和可读。
- 活跃的社区和工具链:Rust 社区非常活跃,官方提供了优秀的包管理工具 Cargo,以及丰富的第三方库和工具,帮助开发者更高效地进行开发。
3、Rust + C# 的应用思路
C# 和 Rust 各有其独特的优势,适用于不同的应用场景:
- C# 更适合需要快速开发、良好 IDE 支持和丰富库的企业级应用,尤其是在 Web 和桌面应用领域。
- Rust 则更适合需要高性能、内存安全和并发编程的系统级编程,如操作系统、嵌入式系统和高性能计算。
结合 Rust 和 C# 的优势,可以创建具有高性能、安全性和易用性的混合应用程序。
3.1、C# 主要负责
- 用户界面(UI):使用 WPF、WinForms 或 UWP 等技术构建桌面应用程序的图形用户界面。使用 ASP.NET Core 或 Blazor 构建 Web 应用程序的前端和后端。
- 业务逻辑:实现企业级应用的业务逻辑,包括数据处理、验证、工作流等。编写与数据库交互的代码,使用 Entity Framework 等 ORM 工具进行数据持久化。
- Web 服务和 API:使用 ASP.NET Core 开发 RESTful API 或 gRPC 服务,提供对外接口。处理身份验证、授权和其他安全相关的功能。
- 快速原型开发:利用 C# 的高效开发工具和丰富的库,快速实现和迭代应用原型。
- 集成和自动化:编写脚本和工具,用于自动化部署、持续集成和持续交付(CI/CD)。
3.2、Rust 主要负责
- 性能关键模块:实现需要极高性能和低延迟的核心计算模块,如图像处理、数据压缩、加密解密等。开发高性能的算法和数据结构,确保系统在高负载下的稳定性和效率。
- 系统编程:编写操作系统内核、驱动程序、嵌入式系统固件等底层代码。实现与硬件直接交互的代码,确保高效和可靠的资源管理。
- 并发和多线程编程:开发需要高度并发和多线程支持的应用,如高性能服务器、实时系统等。利用 Rust 的所有权和借用检查器,确保线程安全,避免数据竞争。
- 安全关键应用:编写涉及敏感数据处理的代码,如身份验证、加密解密、权限管理等。确保代码在运行时的内存安全,防止缓冲区溢出、空指针引用等常见漏洞。
- WebAssembly 模块:将 Rust 代码编译为 WebAssembly,用于在浏览器中执行高性能任务。在 Web 应用中使用 Rust 提供的功能,提高前端性能。
3.3、示例项目
假设我们要开发一个金融分析平台,该平台包括一个桌面应用程序和一个高性能后台服务:
桌面应用程序(C#)
- 使用 WPF 构建用户界面,展示数据分析结果和图表。
- 实现用户登录、权限管理和配置界面。
- 与后台服务通信,发送分析请求并接收结果。
后台服务(Rust)
- 实现高性能的数据处理和分析算法,处理大量金融数据。
- 确保数据处理过程中的内存安全和并发安全。
- 提供 RESTful API 接口,供桌面应用程序调用。
通过这种职责分配,可以充分利用 C# 和 Rust 各自的优势。
4、应用场景
结合 Rust 和 C# 的优势,可以创建具有高性能、安全性和易用性的混合应用程序。以下是一些具体的应用场景和方法:
-
高性能计算模块
Rust:用于编写需要极高性能和内存安全的核心计算模块。例如,图像处理、数据压缩、加密算法等。
C#:用于构建用户界面、业务逻辑和其他高层次的应用部分。
通过这种方式,开发者可以利用 Rust 的性能优势,同时享受 C# 提供的丰富生态系统和开发工具。 -
游戏开发
Rust:用于实现游戏引擎的底层部分,如物理引擎、渲染引擎等,这些部分对性能要求非常高。
C#:用于编写游戏逻辑、用户界面和脚本。Unity 引擎就是一个很好的例子,它主要使用 C# 进行开发。
这种组合可以确保游戏在性能关键的部分表现出色,同时保持开发过程的高效和灵活。 -
微服务架构
Rust:用于编写性能关键的微服务,例如需要处理大量并发请求的服务,或者需要进行复杂数据处理的服务。
C#:用于编写其他微服务,特别是那些涉及到企业级应用逻辑、数据库操作和 Web API 的部分。
通过这种方式,可以在保证整体系统性能的同时,利用 C# 的快速开发和维护优势。 -
嵌入式系统与前端交互
Rust:用于开发嵌入式系统中的固件或驱动程序,这些部分通常需要高度的可靠性和性能。
C#:用于开发与嵌入式系统交互的桌面应用或移动应用,通过网络协议或串口通信与嵌入式设备进行数据交换。
这种组合可以确保嵌入式系统的稳定性和性能,同时提供用户友好的界面和交互体验。 -
安全关键应用
Rust:用于编写需要高度安全性的代码部分,例如身份验证、加密解密、权限管理等。
C#:用于构建应用的其余部分,包括用户界面、数据展示和业务逻辑。
通过这种方式,可以最大限度地减少安全漏洞,同时保持开发效率。
二、技术集成方法
1、FFI(外部函数接口)
使用 Rust 编写的库可以通过 FFI 暴露给 C# 使用。C# 可以调用这些高性能的 Rust 函数,从而将两者的优势结合起来。
2、WebAssembly
Rust 可以编译为 WebAssembly,然后在 C#(例如 Blazor 项目)中使用。这种方法特别适用于 Web 应用开发,使得前端部分也能享受到 Rust 的性能优势。
3、微服务架构
将不同语言编写的服务部署为独立的微服务,通过 HTTP/REST 或 gRPC 等协议进行通信。这种方法使得各个服务可以独立开发和部署,充分利用各自语言的优势。
三、探索 FFI
1、什么是 FFI
FFI 是 “Foreign Function Interface” 的缩写,指的是一种编程接口,它允许一种编程语言调用另一种编程语言的函数或子程序。FFI 通常用于以下几种情况:
- 性能优化:某些任务在特定语言(如 C 或 C++)中执行速度更快,因此可以通过 FFI 调用这些高效的函数。
- 代码重用:利用已有的库和代码,而不必重新实现相同的功能。
- 跨语言协作:在多语言项目中,各个部分可能由不同的编程语言编写,通过 FFI 可以使它们互操作。
例如,在 Python 中,可以使用 ctypes 或 cffi 库来调用 C 语言编写的函数。在 Java 中,可以使用 JNI(Java Native Interface)来调用本地代码。
FFI 的具体实现和使用方法会因编程语言而异,但其核心思想是提供一种机制,使得不同语言之间能够进行函数调用和数据交换。
示例:Rust 调用 C 函数
FFI 主要关注的是如何在源代码层面上进行跨语言调用。具体职责:
- 声明外部函数:提供语法和机制来声明其他语言中的函数。例如,在 Rust 中使用 extern 关键字。
- 类型匹配:确保调用的参数和返回值类型与外部函数的定义相匹配。
- 内存管理:处理跨语言调用时的内存分配和释放问题。
- 安全性:由于跨语言调用涉及到不受当前语言控制的代码执行,FFI 通常需要通过 unsafe 块来显式标记不安全操作。
// main.rs
// 声明外部 C 函数
extern "C" {
fn add(a: i32, b: i32) -> i32;
}
fn main() {
let result = unsafe { add(5, 3) };
println!("The sum is: {}", result);
}
在这个示例中,FFI 的角色体现在 extern “C” 关键字和 unsafe 块上,它们使得 Rust 可以调用 C 编写的 add 函数。
2、C ABI
FFI 的实现通常依赖于底层的 ABI 规范,例如 C ABI。C ABI 是一组规定了函数调用、参数传递、返回值处理、内存布局等细节的规则和约定。它确保了编译后的二进制代码能够正确地进行函数调用和数据交换。C ABI 通常由操作系统和硬件平台定义,并且不同的平台可能有不同的 ABI 规范。
主要内容包括:
- 调用约定:函数参数如何传递(通过寄存器或堆栈)、返回值如何传递、调用者和被调用者的职责。
- 名称修饰:函数名和变量名在编译后的符号表中的表示方式。
- 内存布局:数据结构(如结构体、联合体)的内存对齐和布局方式。
- 错误处理:错误码或异常处理机制。
示例:C 函数
C ABI 主要关注的是在二进制层面上如何进行跨语言调用。具体职责:
- 调用约定:定义函数参数如何传递(通过寄存器或堆栈)、返回值如何传递、调用者和被调用者的职责。
- 名称修饰:规定函数名和变量名在编译后的符号表中的表示方式,以避免命名冲突。
- 内存布局:定义数据结构(如结构体、联合体)的内存对齐和布局方式。
- 错误处理:规定错误码或异常处理机制。
// add.c
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
当我们编译这个 C 文件时,编译器会按照目标平台的 C ABI 规则生成二进制代码,包括函数 add 的参数传递、返回值处理、名称修饰等。
FFI 与 C ABI 的关系
- 依赖关系:FFI 的实现依赖于底层的 ABI 规范。例如,Rust 调用 C 函数时,需要遵循 C ABI 的规则,以确保参数传递、函数调用和返回值处理的正确性。
- 互补关系:C ABI 定义了二进制级别的调用约定和内存布局,而 FFI 提供了语言级别的接口,使得程序员可以在源代码中声明和调用外部函数。
- 跨语言互操作:通过 FFI 和 C ABI,不同编程语言可以互相调用对方的函数,实现跨语言的功能复用和集成。
总结
- FFI 的角色:提供语言级别的接口,使得程序员可以在源代码中声明和调用外部函数,处理类型匹配、内存管理和安全性问题。
- C ABI 的角色:定义二进制级别的调用约定和内存布局,确保不同编程语言生成的二进制代码能够正确地互操作。
通过 FFI 和 C ABI 的协作,不同编程语言可以实现跨语言调用和互操作,从而复用已有的库和功能。
3、C# 导出 C ABI
在 C# 中,可以使用 UnmanagedFunctionPointer 属性和委托来将 C# 方法导出为 C ABI 函数,以便其他非托管代码(如 C/C++)可以调用它。这通常涉及到以下几个步骤:
- 定义一个委托类型,表示要导出的函数签名。
- 使用 UnmanagedFunctionPointer 属性指定调用约定。
- 创建一个静态方法,并将其分配给该委托。
- 获取该委托的函数指针,并将其传递给需要调用的非托管代码。
下面是一个示例,展示了如何在 C# 中定义并导出一个 C ABI 函数:
using System;
using System.Runtime.InteropServices;
class Program
{
// 定义一个委托类型,表示要导出的函数签名
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void HelloDelegate();
// 定义一个静态方法,将其导出为 C ABI 函数
public static void HelloFromCSharp()
{
Console.WriteLine("Hello from C#!");
}
static void Main()
{
// 创建一个委托实例,指向静态方法
HelloDelegate helloDelegate = new HelloDelegate(HelloFromCSharp);
// 获取委托的函数指针
IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(helloDelegate);
// 打印函数指针地址(可选)
Console.WriteLine($"Function pointer: {functionPointer}");
// 这里可以将函数指针传递给需要调用的非托管代码
// 例如,通过 P/Invoke 调用一个 C 函数,并将函数指针作为参数传递
// For demonstration, we will just call the function directly in C#
// 将函数指针转换回委托并调用
HelloDelegate importedDelegate = (HelloDelegate)Marshal.GetDelegateForFunctionPointer(functionPointer, typeof(HelloDelegate));
importedDelegate();
}
}
说明
-
定义委托类型:使用 UnmanagedFunctionPointer 属性定义一个委托类型 HelloDelegate,表示要导出的函数签名。这里指定了 CallingConvention.Cdecl 调用约定。
-
定义静态方法:定义一个静态方法 HelloFromCSharp,这是我们希望导出为 C ABI 函数的方法。
-
创建委托实例:在 Main 方法中,创建一个 HelloDelegate 类型的委托实例,指向静态方法 HelloFromCSharp。
-
获取函数指针:使用 Marshal.GetFunctionPointerForDelegate 方法获取委托的函数指针。
-
传递函数指针:在实际应用中,可以将这个函数指针传递给需要调用的非托管代码(例如通过 P/Invoke 调用一个 C 函数,并将函数指针作为参数传递)。在这个示例中,我们只是将函数指针转换回委托并直接调用它。
通过这种方式,可以将 C# 方法导出为 C ABI 函数,使得其他非托管代码能够调用它。
4、C# 导入函数
通过 DllImport 属性,我们可以在 C# 中声明和调用外部的 DLL 函数。
DllImport 是 C# 中用于调用非托管代码(如 C/C++ 库)的一种机制。它是 .NET 平台调用服务(P/Invoke,Platform Invocation Services)的核心部分。
假设我们有一个 C 库 mylib.c,其中包含一个函数 add,
// mylib.c
#include <stdio.h>
__declspec(dllexport) int add(int a, int b) {
return a + b;
}
编译这个 C 文件生成动态链接库(DLL):
gcc -shared -o mylib.dll mylib.c
在 C# 中,我们使用 DllImport 属性来声明和调用这个 C 函数:
using System;
using System.Runtime.InteropServices;
class Program
{
// 声明外部函数
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int add(int a, int b);
static void Main()
{
int result = add(3, 4);
Console.WriteLine("Result: " + result);
}
}
5、Rust 导出函数
在 Rust 中导出函数以供其他语言(如 C 或 C++)调用,可以使用 #[no_mangle] 和 extern “C” 属性。这些属性确保函数名称不会被编译器修改,并且使用 C 语言的调用约定。
首先,创建一个新的 Rust 库项目:
cargo new --lib my_rust_lib
cd my_rust_lib
修改 Cargo.toml ,
[lib]
crate-type = ["cdylib"]
rlib:Rust库,这是cargo new默认的种类,只能被Rust调用;
dylib:Rust规范的动态链接库,windows上编译成.dll,linux上编译成.so,也只能被Rust调用;
cdylib:满足C语言规范的动态链接库,windows上编译成.dll,linux上编译成.so,可以被其他语言调用
staticlib:静态库,windows上编译成.lib,linux上编译成.a,可以被其他语言调用
编辑 src/lib.rs 文件,添加想要导出的函数。例如:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn hello_from_rust() {
println!("Hello from Rust!");
}
编译这个库生成一个共享库文件。在 Unix 系统上,这将生成一个 .so 文件;在 macOS 上,这将生成一个 .dylib 文件;在 Windows 上,这将生成一个 .dll 文件。
cargo build --release
编译完成后,在 target/release 目录下找到生成的共享库文件,例如 libmy_rust_lib.so、libmy_rust_lib.dylib 或 my_rust_lib.dll。
接下来,创建一个名为 main.c 的文件来调用 Rust 导出的函数,添加以下代码:
#include <stdio.h>
// 声明从 Rust 导入的函数
int add(int a, int b);
void hello_from_rust();
int main() {
int result = add(3, 4);
printf("Result of add: %d\n", result);
hello_from_rust();
return 0;
}
在编译和链接 C 程序时,需要指定 Rust 生成的共享库:
gcc main.c -o main -L./target/release -lmy_rust_lib -ldl -Wl,-rpath,./target/release
最后,运行生成的可执行文件:
./main
如果一切正常,你应该会看到输出:
Result of add: 7
Hello from Rust!
注意事项
- 调用约定:确保 Rust 和 C 之间的调用约定一致。在上面的示例中,我们使用 extern “C” 调用约定。
- 字符串处理:如果需要传递字符串,请注意 Rust 和 C 之间的字符串编码和内存管理差异。
- 错误处理:处理可能的错误,例如 DLL 加载失败或函数获取失败。
6 、C# 导出函数(补充)
方式 1、DllExport
通过 DllExport 属性,我们可以将 C# 方法导出为非托管代码,使其可以被其他语言(如 C++、Python 等)调用。
DllExport 属性在 C# 中并不是原生存在的属性,而是通过第三方工具(如 UnmanagedExports 或 DllExport)实现的。
安装 DllExport 工具
安装 UnmanagedExports:这是一个常用的 NuGet 包,可以帮助我们导出 C# 函数。
dotnet add package UnmanagedExports --version 1.2.7
参考资料
https://www.nuget.org/packages/UnmanagedExports
或者使用 DllExport:
参考资料
https://www.nuget.org/packages/DllExport
https://github.com/3F/DllExport/wiki/Quick-start
https://github.com/3F/DllExport
使用 DllExport 属性
在 C# 项目中使用 DllExport (UnmanagedExports 包)属性来导出函数。
namespace MyLib;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;
public class MyClass
{
[DllExport("Add", CallingConvention = CallingConvention.Cdecl)]
public static int Add(int a, int b)
{
return a + b;
}
}
参数说明
- “Add”:这是导出的函数名。在非托管代码中,这个名字将用于引用该函数。
- CallingConvention.Cdecl:这是调用约定。常见的调用约定包括 Cdecl、StdCall 和 ThisCall。选择合适的调用约定非常重要,因为这会影响到函数参数的传递方式和堆栈清理方式。
编译生成 DLL
编译项目,将生成一个 DLL 文件。这个 DLL 文件包含了我们导出的函数。
注意事项
- 平台一致性:确保目标平台(x86 或 x64)与 DLL 的编译平台一致,以避免兼容性问题。
- 调用约定:确保在导出和调用时使用相同的调用约定。
- 错误处理:在加载 DLL 和获取函数地址时,添加适当的错误处理代码。
方式 2、Native AOT(微软官方)
Native AOT 简介?
Native AOT(Ahead-of-Time)是指在编程语言和运行时环境中,将代码在编译阶段提前转换为机器码,而不是在运行时进行即时编译(JIT)。这种方法可以带来多种好处,包括更快的启动时间、减少内存使用以及提高性能。
以下是 Native AOT 的一些关键点:
-
提前编译:代码在开发阶段就被编译成目标平台的机器码,这样在运行时不需要再进行编译,直接执行机器码即可。
-
性能提升:由于省去了运行时编译的开销,应用程序的启动速度和整体性能通常会有所提升。
-
减少内存占用:因为不需要 JIT 编译器和相关的数据结构,内存占用可能会减少。
-
部署简化:生成的二进制文件包含了所有必要的依赖项,可以更容易地进行分发和部署。
-
安全性:由于没有中间语言代码存在,反编译和逆向工程变得更加困难,从而增加了代码的安全性。
-
平台特定优化:编译器可以针对特定平台进行优化,从而进一步提升性能。
Native AOT 在不同的编程语言和框架中有不同的实现。.NET 6 引入了对 Native AOT 的支持,使得 .NET 应用程序可以编译成本地代码,从而获得上述的各种优势。
使用示例
参考:https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs
创建类库项目,
dotnet new classlib -o AotDll -f net8.0
AotDll.csproj 配置 AOT Native,
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>
导出非托管函数,
using System.Runtime.InteropServices;
namespace AotDll
{
public class Class1
{
// 无参数有返回值
[UnmanagedCallersOnly(EntryPoint = "IsOk")]
public static bool IsOk()
{
return true;
}
// 有参数无返回值
[UnmanagedCallersOnly(EntryPoint = "MyPrinter")]
public static void MyPrinter(IntPtr pString)
{
try
{
if (pString != IntPtr.Zero)
{
string str = new(Marshal.PtrToStringAnsi(pString));
Console.WriteLine(str);
}
}
catch (Exception e)
{
Console.WriteLine(">>> Exception " + e.Message);
}
}
// 有参数有返回值
[UnmanagedCallersOnly(EntryPoint = "MyConcat")]
public static IntPtr MyConcat(IntPtr pString1, IntPtr pString2)
{
string concat = "";
try
{
if (pString1 != IntPtr.Zero && pString2 != IntPtr.Zero)
{
string str1 = new(Marshal.PtrToStringAnsi(pString1));
string str2 = new(Marshal.PtrToStringAnsi(pString2));
concat = string.Concat(str1, str2);
}
}
catch (Exception e)
{
concat = e.Message;
}
return Marshal.StringToHGlobalAnsi(concat);
}
// 无参数无返回值
[UnmanagedCallersOnly(EntryPoint = "PrintHello")]
public static void PrintHello()
{
Console.WriteLine(">>> Hello");
}
}
}
查看导出结果,
# -r 参数 win-x64/linux-x64/osx-arm64
# -p:NativeLib=Static 静态库
dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r linux-x64 -c release
nm -D AotDll.so
可以看到函数正常导出。
7、Rust 导入函数(补充)
libloader 动态加载函数
libloader 是基于 libloading 的,但是操作起来会比 libloader 方便。
https://docs.rs/crate/libloader/latest
[dependencies]
libloader = "0.1.4"
use cstr::cstr;
use libloader::*;
use std::{ffi::CStr,os::raw::c_char};
fn main() {
get_libfn!("dll/mydll.dll", "println", println, (), s: &str);
println("你好");
get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);
println!(" 1 + 2 = {}", add(1, 2));
get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);
print_hello();
get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);
let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };
print!("out {}", str.to_str().unwrap());
}
libloading 动态加载函数
[dependencies]
libloading = "0.8"
fn call_dynamic() -> Result<u32, Box<dyn std::error::Error>> {
unsafe {
let lib = libloading::Library::new("/path/to/liblibrary.so")?;
let func: libloading::Symbol<unsafe extern fn() -> u32> = lib.get(b"my_func")?;
Ok(func())
}
}
四、互调示例
实现 C# 调用 Rust 处理一个异步耗时操作,然后让 Rust 调用 C# 方法输出日志:
- Step1、在 Rust 中编写一个异步函数,并使用 FFI 暴露给 C#。
- Step2、在 C# 中调用这个 Rust 函数,并提供一个 callback 回调函数(C ABI)用于日志输出。
- Step3、Rust 执行完耗时操作后,调用 callback 在 C# 端输出日志。
Part1、Rust 部分
创建 Rust 库项目,
cargo new --lib my_ffi_rust_lib
cd my_ffi_rust_lib
修改配置文件,
# Cargo.toml
[package]
name = "my_ffi_rust_lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
修改 src/lib.rs 代码,
// src/lib.rs
use std::ffi::CString;
use std::os::raw::c_char;
use std::sync::mpsc; // 引入多生产者单消费者通道模块
use std::thread::{self}; // 引入线程模块
use std::time::Duration; // 引入时间模块
type LogCallback = extern "C" fn(*const c_char);
#[no_mangle]
pub extern "C" fn start_async_operation(callback: LogCallback) {
// 创建一个多生产者单消费者通道
let (tx, rx) = mpsc::channel();
// 启动一个新线程
let _ = thread::spawn(move || {
// 在线程中发送一个信号,表示线程已经启动
tx.send(()).unwrap();
// 休眠100毫秒,模拟耗时操作
thread::sleep(Duration::from_millis(2000));
// Prepare the log message
let message = CString::new("Operation completed").unwrap();
// Call the .NET logging function
callback(message.as_ptr());
});
// 接收线程启动信号,确保线程已成功启动
rx.recv().unwrap();
}
Part2、C# 部分
新建一个控制台项目,在 C# 中定义一个托管的回调函数,并调用 Rust 的异步函数。
// Program.cs
using System;
using System.Runtime.InteropServices;
using System.Threading;
class Program
{
// Define the delegate for the callback
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LogCallback(IntPtr message);
// Import the Rust function
[DllImport("libmy_ffi_rust_lib.so", CallingConvention = CallingConvention.Cdecl)]
public static extern void start_async_operation(LogCallback callback);
// The managed callback function
public static void LogMessage(IntPtr message)
{
string msg = Marshal.PtrToStringAnsi(message);
Console.WriteLine(msg);
}
static void Main(string[] args)
{
// Create the callback delegate
LogCallback callback = new LogCallback(LogMessage);
// Start the async operation
start_async_operation(callback);
// Keep the application running to wait for the async operation to complete
Console.WriteLine("Waiting for async operation to complete...");
Thread.Sleep(6000); // Sleep longer than the async operation duration
}
}
Part3、编译和运行
编译Rust库
cargo build
将生成的共享库(libmy_ffi_rust_lib.so)复制到 C# 项目的输出目录。
编译并运行 C# 程序
dotnet run
Waiting for async operation to complete...
Operation completed
可以看到控制台启动后,调用了 Rust 端执行了一个耗时操作,Rust 端执行完毕后,回调 C# 的 LogMessage 方法输出操作结果。
参考 https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.interopservices.unmanagedfunctionpointerattribute?view=net-8.0
总结
通过以上示例,我们实现了 C# 调用 Rust 处理一个异步耗时操作,并且 Rust 在操作完成后调用 C# 方法输出日志。这种方式利用了 FFI 和回调机制,实现了跨语言的异步操作和日志输出。
结合 Rust 和 C# 的优势,可以创建既高性能又易于开发和维护的应用程序。在实际项目中,根据具体需求选择合适的技术栈,并通过合理的架构设计,将两者的优点最大化。
原文地址:https://blog.csdn.net/weixin_47560078/article/details/143806916
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!