自学内容网 自学内容网

探索 FFI - Rust 与 C# 互调实战

所谓幸福,就是把灵魂安放在适当的位置。
—— 亚里士多德 Aristotle

一、Rust + C# = ?

1、C# 的优势

  1. 丰富的生态系统:C# 是由微软开发和维护的,拥有强大的 .NET 框架支持,提供了大量的库和工具,可以极大地提高开发效率。Visual Studio 是一个功能强大的集成开发环境(IDE),为 C# 开发提供了卓越的支持。
  2. 跨平台能力:随着 .NET Core 的推出,C# 现在可以在 Windows、Linux 和 macOS上运行,实现了真正的跨平台开发。
  3. 面向对象编程:C# 是一种纯粹的面向对象编程语言,支持类、继承、多态等特性,使得代码结构清晰,易于维护和扩展。
  4. 企业级应用开发:C# 广泛应用于企业级应用开发,特别是在 Web 应用、桌面应用和云服务方面,如 ASP.NET 用于构建高性能的 Web 应用和 API。
  5. 社区和支持:拥有庞大的开发者社区和丰富的在线资源,包括文档、教程和论坛,帮助开发者快速解决问题。

2、Rust 的优势

  1. 内存安全:Rust最显著的特点是其内存安全性,通过所有权系统和借用检查器,在编译时防止数据竞争、空指针引用和缓冲区溢出等常见的内存错误。
  2. 高性能:Rust 的性能接近 C 和 C++,因为它是编译型语言,并且没有运行时开销,非常适合系统编程和性能关键的应用。
  3. 并发编程:Rust 提供了强大的并发编程模型,通过所有权和类型系统来确保线程安全,避免了传统多线程编程中的许多陷阱。
  4. 现代化设计:Rust 结合了现代编程语言的许多优点,如模式匹配、闭包、泛型和函数式编程风格,使得代码更加简洁和可读。
  5. 活跃的社区和工具链: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# 的优势,可以创建具有高性能、安全性和易用性的混合应用程序。以下是一些具体的应用场景和方法:

  1. 高性能计算模块
    Rust:用于编写需要极高性能和内存安全的核心计算模块。例如,图像处理、数据压缩、加密算法等。
    C#:用于构建用户界面、业务逻辑和其他高层次的应用部分。
    通过这种方式,开发者可以利用 Rust 的性能优势,同时享受 C# 提供的丰富生态系统和开发工具。

  2. 游戏开发
    Rust:用于实现游戏引擎的底层部分,如物理引擎、渲染引擎等,这些部分对性能要求非常高。
    C#:用于编写游戏逻辑、用户界面和脚本。Unity 引擎就是一个很好的例子,它主要使用 C# 进行开发。
    这种组合可以确保游戏在性能关键的部分表现出色,同时保持开发过程的高效和灵活。

  3. 微服务架构
    Rust:用于编写性能关键的微服务,例如需要处理大量并发请求的服务,或者需要进行复杂数据处理的服务。
    C#:用于编写其他微服务,特别是那些涉及到企业级应用逻辑、数据库操作和 Web API 的部分。
    通过这种方式,可以在保证整体系统性能的同时,利用 C# 的快速开发和维护优势。

  4. 嵌入式系统与前端交互
    Rust:用于开发嵌入式系统中的固件或驱动程序,这些部分通常需要高度的可靠性和性能。
    C#:用于开发与嵌入式系统交互的桌面应用或移动应用,通过网络协议或串口通信与嵌入式设备进行数据交换。
    这种组合可以确保嵌入式系统的稳定性和性能,同时提供用户友好的界面和交互体验。

  5. 安全关键应用
    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 通常用于以下几种情况:

  1. 性能优化:某些任务在特定语言(如 C 或 C++)中执行速度更快,因此可以通过 FFI 调用这些高效的函数。
  2. 代码重用:利用已有的库和代码,而不必重新实现相同的功能。
  3. 跨语言协作:在多语言项目中,各个部分可能由不同的编程语言编写,通过 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 文件包含了我们导出的函数。

注意事项

  1. 平台一致性:确保目标平台(x86 或 x64)与 DLL 的编译平台一致,以避免兼容性问题。
  2. 调用约定:确保在导出和调用时使用相同的调用约定。
  3. 错误处理:在加载 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)!