估计阅读时长: 9 分钟

因为一种单一的编程语言并不会覆盖到所有的适用场景的原因,在一个软件工程项目之中,采用多种语言进行混合编程是一种很常见的协作方式。例如,脚本化的语言,其非常适合于进行最顶层的应用开发,就像胶水一样用于将各种组件进行粘贴,但是脚本化的语言自身因为是基于其他的语言所构建,所以执行效率一般不会太好。对于底层组件,我们一般就会需要使用静态编译类型的非托管语言创建用于高性能数据处理的模块。对于这种需求的底层模块,我们一般可以采用C/C++/Rust来编写。

基于最近在一些商业项目开发中的实践发现,目前所开发的R#脚本语言在某些场景的计算性能上并不会达到太高的计算效率,所以针对这种计算效率的需求,为R#脚本引擎创建了用于调用rust动态链接库中的函数的polyglot programming语言特性。

Rust是一种高度并发和高性能的语言,专注于安全和速度、内存管理和编写干净的代码。它还保证线程安全,其目的是提高现有应用程序的性能。它得到了 Mozilla 的支持,以解决并发的关键问题。数以百计的公司,无论规模大小,都在生产中使用 Rust 来完成各种任务。这些任务包括命令行工具、web 服务、DevOps 工具、嵌入式设备、音视频分析与转码、加密货币(cryptocurrencies)、生物信息学(bioinformatics)、搜索引擎、物联网(internet of things, IOT)程序、机器学习,甚至还包括 Firefox 浏览器的大部分内容。

在VB代码中静态调用rust函数

假设我们已经编写好了一个rust函数库供.NET程序进行动态调用,那么我们可以非常轻松的在代码中进行类似于如下所示的静态代码申明:

<DllImport("libdl", EntryPoint:="dlsym")>
Private Shared Function dlsym(hModule As IntPtr, 
                              <MarshalAs(UnmanagedType.LPStr)> 
                              lpProcName As String) As IntPtr
End Function

从上面的静态申明代码之中我们也可以了解到,如果需要在VB代码之中进行外部的动态链接库的函数调用,我们至少应该需要知道两种信息:动态链接库的位置(既模块文件名)以及调用的参数信息。

在运行时动态调用rust函数

基于前面的PInvoke静态代码我们可以非常容易的进行外部rust函数库的调用,但是这种静态代码调用必须要基于一个前提:就是我们已经知道了函数库的名称,函数名,函数参数类型信息,函数返回值等信息,所以我们可以将这一切动态调用的信息提前以代码的形式编译在我们的程序之中。相比较于这种静态调用,在进行脚本化运行的时候,既程序运行时通过用户在脚本之中的脚本输入信息来进行外部的rust动态链接库的动态调用,却存在有非常多的困难点。下面我将在开发过程中所遇到的难点进行一一讲解:

1. dyn.load动态加载函数库

在R语言之中存在有一个名称为dyn.load的函数用于加载基于C、C++、Fortran,Java,Rust等语言编写的动态链接库。所以在R#语言之中也将会通过模仿R语言的dyn.load函数进行rust动态链接库的加载操作。在外部的动态链接库的加载方面的相关知识,我是一窍不通的,但是非常幸运的是,在之前所使用的R.NET项目之中,存在有一个类似的需求,就是在.NET程序运行时加载R环境的动态链接库。R.NET的作者在Github上开源进行这一动态加载的工具代码(https://github.com/rdotnet/dynamic-interop-dll),这样子借助于这个项目,我们就可以实现类似于dyn.load的动态加载工作了:

2. 自动化动态构建函数指针

在上面所提到的DynamicInterop项目中,作者给出来进行动态调用的代码示例:

MY_C_API_MARKER void Play(void* simulation, 
                          const char* variableIdentifier, 
                          double* values, 
                          TimeSeriesGeometry* geom);

假设有上面所示的一个C语言函数,那么通过上面的代码库进行动态调用,实际上仍然需要一个函数指针来为.NET CLR运行时提供相关的函数调用信息。在这个函数之中之中,则需要提供相关的参数信息,函数的返回值类型等必要信息。

private delegate void Play_csdelegate(
    IntPtr simulation, 
    string variableIdentifier, 
    IntPtr values, 
    IntPtr geom);

// and somewhere in a class a field is set:
NativeLib = new UnmanagedDll(someNativeLibFilename);

void Play_cs(IModelSimulation simulation, string variableIdentifier, double[] values, ref MarshaledTimeSeriesGeometry geom)
{
    IntPtr values_doublep, geom_struct;
    // here glue code here to create native arrays/structs
    NativeLib.GetFunction<Play_csdelegate>("Play")(CheckedDangerousGetHandle(simulation, "simulation"), variableIdentifier, values_doublep, geom_struct);
    // here copy args back to managed; clean up transient native resources.
}

现在问题来了,我们要如何在.NET CLR运行时环境中动态的创建这样子的一个函数指针类型呢?答案就是Emit和反射!如果我们通过类型查看我们平常可能接触到的Delegate类型申明的话,实际上可以发现在CLR环境之中,我们用到的Delegate函数指针类型实际上就是一种具体的类。假若我们了解怎样通过Emit工具进行Class的动态创建的话,那么在CLR之中进行不同类型的函数指针的创建工作实际上就非常简单了。

因为在这里,我们所需要创建的函数指针是派生于MulticastDelegate类的一种子类,所以我们可以通过下面所示的和创建普通Class类一样的方式创建我们的函数指针类型:

Public Function GetDelegate() As Type
    Dim tdelegate As TypeBuilder = DynamicType.GetTypeBuilder("native_delegate_func", GetType(MulticastDelegate), isAbstract:=True, sealed:=True)
    Dim new_fp = tdelegate.DefineConstructor(MethodAttributes.RTSpecialName Or
                                             MethodAttributes.HideBySig Or
                                             MethodAttributes.Public,
                                             CallingConventions.Standard, {GetType(Object), GetType(IntPtr)})
    Dim params As Type() = parameters.Select(Function(a) a.Value).ToArray
    Dim native_calls = tdelegate.DefineMethod(external_native_call_name,
                                              MethodAttributes.RTSpecialName Or
                                              MethodAttributes.Public Or
                                              MethodAttributes.HideBySig Or
                                              MethodAttributes.NewSlot Or
                                              MethodAttributes.Virtual, CallingConventions.Standard, returnVal, params)

    new_fp.SetImplementationFlags(MethodImplAttributes.CodeTypeMask)
    native_calls.SetImplementationFlags(MethodImplAttributes.CodeTypeMask)

    Return tdelegate.CreateType
End Function

从上面所展示的代码我们可以看得到,创建目标函数指针,实际上就是创建出一个普通的Class类,在上面我们通过设定类名称构建出TypeBuilder对象,然后往里面添加一个Invoke函数的接口即可。相比较于普通的Class类型的创建,在进行Delegate函数指针创建的过程中我们有几点需要额外的留意:

  1. 所有的Delegate函数指针都是抽象类型,所以我们在构建TypeBuilder的时候,应该要设定对应的类为Abstract并且应该是Sealed状态,不可以被继承
  2. 既然我们创建的是一个函数指针,所以函数的具体实现并不是我们所关心的,所以Invoke函数应该是覆写MulticastDelegate基类中的Invoke函数的虚函数,既应该被标记为Virtual
  3. 函数指针的基类MulticastDelegate有一个带参数的构造函数,所以在我们创建的Delegate指针类型的构造函数也因该按照相同的形式调用MulticastDelegate的构造函数

那现在拥有了目标函数指针类型之后,我们就可以通过DynamicInterop项目中所提供的GetFunction函数进行动态链接库中的目标函数的获取了:

' pinvoke.GetDelegate create the function pointer type in
' runtime via Emit
Dim native_func = UnmanagedDll.GetFunction(name, pinvoke.GetDelegate)

3. 字符串传值

我们在进行函数调用的时候,传递字符串参数或者得到字符串值很明显是一种无法避免的场景。对于C/rust语言而言,字符串会分为两种情况:一种是在编译时产生的字符串常量,以及在运行时产生的字符串变量。对于上面的两种情况,前者是一种静态数据,后者则是运行时中的一个数据结构实例。虽然.NET CLR运行时环境之中的PInvoke技术已经帮我们处理好了大部分的数据转换工作,但是对于大部分的新手而言,我们在做字符串传参的时候任然可能会碰到比较大的麻烦。

在传参的时候,因为我们需要将.NET CLR托管环境之中的字符串数据传递进入非托管运行时环境,所以对于字符串数据而言,其会被转换为静态数据而被传入我们编写好的rust环境中,所以我们的rust函数,字符串参数应该是一种指针类型,例如:*const c_char。在进行字符串类型的值从rust非托管运行时环境返回.NET CLR托管运行时环境的时候,我们同样也必须要使用字符串数据的指针进行数据的返回,所以我们需要将字符串返回值类型设定为*mut c_char,下面就是一个例子:

use libc::c_char;
use std::ffi::CStr;
use std::ffi::CString;
use std::str;

#[no_mangle]
pub extern "C" fn get_rust_str(str: *const c_char) -> *mut c_char {
    let data = mkstr(str);
    let mut rust_str = String::from("this is the string from rust, ");

    rust_str.push_str("and get value from R#:");
    rust_str.push_str(data.as_str());
    rust_str.push_str("; that's it!");

    let cstr = CString::new(rust_str).expect("memory error!");

    return cstr.into_raw();
}

上面所展示的代码功能比较简单,如果我们仔细观察里面的代码,可以会发现有一个名字叫做mkstr的函数,这个函数就是用于将我们从CLR环境中传递过来的字符串常量转换为可以在rust运行时中被操作的字符串变量值:

/**
 * cast C string to rust string
*/
fn mkstr(s: *const c_char) -> String {
    let c_str: &CStr = unsafe { CStr::from_ptr(s) };
    let r_str = c_str.to_str().unwrap();
    String::from(r_str)
}

测试rust函数调用

编写rust函数库

因为我们需要创建可以被CLR环境所动态引用的函数库,所以我们的rust程序应该是输出动态链接库类型的项目,所以我们应该配置我们的rust项目为staticlib以及cdylib类型。相关的测试RUST函数库项目的配置如下:

[package]
name = "delaunay"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
libc = "0.2"

[lib]
name = "delaunay"
crate-type = ["staticlib", "cdylib"]

编写可以外部引用的rust函数

接着我们就可以开始编写我们的rust程序代码,进行函数导出了。对于rust编程语言而言,进行函数导出非常的简单,我们会需要将我们的目标函数标记上几个特别的信息:

  1. #[no_mangle]是一种与CLR中的自定义属性类型的语言标记,这个标记会让rust的编译器不对我们的函数名进行自动混淆或者自动修改。
  2. extern "C"这部分就是将我们的函数标记为可以被C语言环境下访问的公共API函数。
#[no_mangle]
pub extern "C" fn demo_rust_func(x: i32) -> i32 {
    println!("Hello from Rust");
    return 42 + x;
}

编译rust模块

基于上面所展示的简单例子的代码,我们就可以进行rust项目的编译,构建出动态链接库程序文件,在rust项目文件夹中,打开命令行,然后执行cargo的编译命令:

cargo build --release

假若如下图所示,没有任何红色的错误提示消息的话,那我们就已经成功编译出了一个rust函数库,可以接下来进行后面的测试工作。

编写R#调用脚本

在R#脚本之中进行rust函数库的调用,需要经过两步:

  1. dyn.load加载目标动态链接库文件
  2. .Call函数进行目标函数的调用

例如,我们针对上面的例子,可以编写出如下的R#脚本进行测试:

# load the dynamic link library file which is
# generated from the rust release build
dyn.load("/path/to/rust_dyn_library/delaunay.dll");

# call target rust function
#   a. delaunay - is the file basename(without file extension suffix) of delaunay.dll
#   b. demo_rust_func - is the target function name in rust library
#   c. x = i32(66) - is the parameter of the target function
#   d. return = "i32" - indicates that the target function returns an integer value
val = .Call("demo_rust_func", "delaunay", x = i32(66), return = "i32");
print(val);

接着我们就可以在R#环境下执行上面的脚本查看rust函数的调用结果了:

很好,在我们的demo测试rust函数中打印出了Hello from Rust,并且42+66等于108!

谢桂纲
Latest posts by 谢桂纲 (see all)

Attachments

  • rust • 162 kB • 80 click
    25.03.2023

  • dyn-load • 67 kB • 80 click
    25.03.2023

  • cargo_build • 23 kB • 73 click
    25.03.2023

  • run_rust • 33 kB • 76 click
    25.03.2023

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

博客文章
April 2024
S M T W T F S
 123456
78910111213
14151617181920
21222324252627
282930  
  1. 空间Spot结果在下载到的mzkit文件夹中有示例吗?我试了试,不是10X结果中的tissue_positions_list.csv(软件选择此文件,直接error);在默认结果中也没找到类似的文件;