RUST FFI

研究了一下rust的外部函数调用这里做一下记录.

实验 win平台下rust调用c函数.

win平台下rust有两中工具链,一种是mingw,另一种是msvc

首先声明,下文说的mingw环境指的是在win10上下载的带有msys的mingw32环境,而rust的mingw工具链指的是stable-x86_64-pc-windows-gnu

使用rustup安装win rust mingw工具链不需要额外安装软件,但是安装win rust msvc工具链会自动查询是否已经安装visual studio,如果没有会要求安装.这里的区别是rust编译器自己不提供链接器,而是在最后的链接步骤使用这两种工具链各自的的链接器.

问题来了msvc工具链的链接器在下载msvc时会被安装,但是mingw工具链呢,为什么不需要额外下载mingw环境而是只需要安装工具链,工具链自己提供了链接器吗?答案是肯定的.

mingw工具链的链接器在.rustup目录的mingw工具链目录中(注意不是在工具链的bin目录,而是在工具链目录的其它子目录里,因为我的电脑已经卸载了该工具链,所以没有展示)链接器的名字是x86_64-w64-mingw32-gcc(如果我记得没错的话,还有一份readme文件,里面说不能使用x86_64-w64-mingw32-gcc来编译c文件,这份文件只做链接器使用).

回到ffi,我一开始是尝试mingw环境编译c文件,然后在rust的mingw工具链中链接它,但是一直无效.我自己认为的原因是正常编译的c文件用的ming环境链接器为gcc,而rust mingw工具链用的的链接器是自带的x86_64-w64-mingw32-gcc.这两种链接器的行为可能有些不一样.(可能链接的库文件和其它一些不知道的原因造成的).所以我决定再使用纯msvc的ffi.

FFI实验过程

//这是c源文件
//foo.c
int fooBar(int a) {
    return 999;
}

 打开x64 Native Tools Command Prompt for VS 2022,并进入源文件目录

D:\CProjectDmo\24_6_30\demo3>cl /c foo.c #这里生成foo.obj
Microsoft (R) C/C++ Optimizing Compiler Version 19.40.33811 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

foo.c 

D:\CProjectDmo\24_6_30\demo3>lib foo.obj /out:foo.lib #这里生成foo.lib
Microsoft (R) Library Manager Version 14.40.33811.0
Copyright (C) Microsoft Corporation.  All rights reserved.


D:\CProjectDmo\24_6_30\demo3>llvm-nm.exe foo.lib #查看里面的函数符号

foo.obj:
01048413 a @comp.id
80010190 a @feat.00
00000003 a @vol.md
00000000 T fooBar #函数名没有被mangle

 构建rust工程 准备实验链接.copy foo.lib到rust工程目录下.以下展示工程结构

PS C:\Users\ltzx2\RustroverProjects\24_07_01\demo1> dir #这里展示工程目录


    Directory: C:\Users\ltzx2\RustroverProjects\24_07_01\demo1


Mode                 LastWriteTime         Length Name                                                                                        
----                 -------------         ------ ----
d-----          7/1/2024   2:39 PM                .idea
d-----          7/1/2024   2:39 PM                src
d-----          7/1/2024   2:37 PM                target
-a----          7/1/2024   2:37 PM              8 .gitignore
-a----          7/1/2024   2:37 PM            149 Cargo.lock
-a----          7/1/2024   2:37 PM             76 Cargo.toml
-a----          7/1/2024   2:31 PM            836 foo.lib

PS C:\Users\ltzx2\RustroverProjects\24_07_01\demo1> cd src
PS C:\Users\ltzx2\RustroverProjects\24_07_01\demo1\src> dir


    Directory: C:\Users\ltzx2\RustroverProjects\24_07_01\demo1\src


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          7/1/2024   2:39 PM            157 main.rs


 展示main.rs

#[link(name = "./foo",kind = "static")]
extern "C"{
    fn fooBar(i:i32)->i32;
}

fn main() {
    let unuse_param = 10;
    let ret = unsafe { fooBar(unuse_param) };
    println!("{}",ret);
}

展示运行结果

PS C:\Users\ltzx2\RustroverProjects\24_07_01\demo1\src> cargo run
warning: `C:\Users\ltzx2\.cargo\config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `C:\Users\ltzx2\.cargo\config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
     Running `C:\Users\ltzx2\RustroverProjects\24_07_01\demo1\target\debug\demo1.exe`
999 #这里是输出结果

记录一些实验背后学习到的经验知识

ABI二进制接口

在rust代码中上面是标准的调用ffi方法.用到了extern关键字,这个关键字是用来指定编译成汇编文件所使用的abi规范的.我在使用中发现了一个有趣的现象,下面我来展示一下.


extern "cdecl"{
    fn fooBar(i:i32)->i32;
}



extern "C"{
    fn fooBar(i:i32)->i32;
}

rust有两种extern abi都可以生成c abi的调用约定,并且都能使程序运行成功.但是"cdecl"和"C"的区别是什么呢?经过查找有以下结论.

rust extern"cdecl"指的是c语言特定在x86/64平台上的约定而extern "C"则可以在任意平台下的c语言调用约定所以代码写成extern"C"可以适用所有支持c语言和rust语言的硬件平台

也就是说extern "C"和extern "cdecl"在我的x86/64 win10下是等价的.

还有一些有趣的知识.下面展示另一种ffi调用的方法.

ASM!内嵌汇编

我们修改一下c文件,并把它制作成.lib文件,

//foo.c
int fooBar(int a) {
    return 999+a;
}

接着把.lib文件copy到rust工程下并覆盖原.lib文件.修改main.rust,

use std::arch::asm;

#[link(name = "./foo", kind = "static")]
extern "C" {}

fn main() {
    let mut param = 11;
    let mut ret = 0;
    unsafe {
        asm!(
        "call fooBar;",
        in("ecx")param, out("eax")ret);
    }
    println!("{}", ret);
}

并运行.输出结果为1010.这样也成功完成了ffi调用.

让我们看看对rust文件做了哪些修改.

  1. 我们在extern语句块不再声明外部函数.
  2. 使用了内嵌汇编asm!宏.
  3. 在宏内直接使用lib文件中的函数符号fooBar 并使用call指令调用.
  4. 把param变量的值传递给了ecx寄存器,并在声明在内嵌汇编执行完后使用ret变量接收eax寄存器的值.

解释第2点asm!宏可以让我们在rust代码中插入汇编指令,并混合在最后的可执行文件中.

解释第3点,由于我们使用了#[link(name = "./foo", kind = "static")]那么程序在执行链接时会对我们的foo.lib文件一起处理链接.而通过llvm-nm.exe foo.lib命令可以看到我们编译的函数的符号名并没有被改变,依然是fooBar.所以我们可以使用call fooBar指令.这个行为在链接器层面是完全可以通过的.

解释完第2,3点后我们再来看第一点,既然使用了内嵌汇编和link声明那么完全没有必要再申明一个外部符号供源码调用.可以直接使用call指令.这也就是为什么在extern语句块中不用再声明外部函数的原因.

最后一点要从汇编的角度看.我们先执行

cargo install cargo-asm

他会帮我们使用rust包管理工具下载并编译cargo-asm项目的源码,生成可执行文件供我们使用.

cargo-asm.exe的作用是帮助我们查看编译的rust文件的中间汇编代码. 我们先看一下foo.c文件的汇编代码,再看一下main.rust文件的汇编代码.

查看foo.c文件的汇编代码.

D:\CProjectDmo\24_6_30\demo3>llvm-objdump.exe foo.obj -d #查看fooBar函数的汇编代码

foo.obj:        file format coff-x86-64

Disassembly of section .text$mn:

0000000000000000 <fooBar>:
       0: 89 4c 24 08                   movl    %ecx, 8(%rsp)
       4: 8b 44 24 08                   movl    8(%rsp), %eax
       8: 05 e7 03 00 00                addl    $999, %eax              # imm = 0x3E7
       d: c3                            retq

 查看main.rust的汇编代码.

PS C:\Users\ltzx2\RustroverProjects\24_07_01\demo1\src> cargo asm demo1::main
warning: `C:\Users\ltzx2\.cargo\config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `C:\Users\ltzx2\.cargo\config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
demo1::main:
 sub     rsp, 104
 mov     ecx, 11
 #APP
 call    fooBar
 #NO_APP
 mov     dword, ptr, [rsp, +, 36], eax
 lea     rax, [rsp, +, 36]
 mov     qword, ptr, [rsp, +, 40], rax
 lea     rax, [rip, +, _ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h5c33af0abd501797E]
 mov     qword, ptr, [rsp, +, 48], rax
 lea     rax, [rip, +, __unnamed_2]
 mov     qword, ptr, [rsp, +, 56], rax
 mov     qword, ptr, [rsp, +, 64], 2
 mov     qword, ptr, [rsp, +, 88], 0
 lea     rax, [rsp, +, 40]
 mov     qword, ptr, [rsp, +, 72], rax
 mov     qword, ptr, [rsp, +, 80], 1
 lea     rcx, [rsp, +, 56]
 call    std::io::stdio::_print
 nop
 add     rsp, 104
 ret

观察可以发现c代码先是从ecx寄存器获取输入参数再执行addl指令最后把结果保存在eax寄存器中所以在使用内嵌汇编时如果想要传递参数给fooBar也需要相应的先把值放入ecx寄存器最后再从eax中取出返回值.我们的rust汇编代码正是这么做的.

上面涉及到的汇编层面寄存器的使用约定和函数名称约定,就是abi(二进制接口)规范中的

  • 参数和返回值放置的位置(在寄存器中;在调用栈中;两者混合)规范
  • 名称修饰 规范

上面展示的是rust静态链接c库,但是还有一种是动态链接dll这该怎么实现呢.如果你在跟着尝试可能会发现rust的#link声明kind特征还可以有一个值是dylib.我一开始也认为只需要name特征指向.dll然后kind设置为dylib就可以但是事实却不是这样.于是开始查找资料,最终看到了这篇post.Using DLL Functions in Rusticon-default.png?t=N7T8https://www.rangakrish.com/index.php/2022/05/04/using-dll-functions-in-rust/

Using DLL Functions in Rust

Written by admin MAY 4, 2022in C++ProgrammingRust with 1 Comment

When you program in Rust, especially in a non-trivial project, there is a good chance that you will need to call “external” functions (usually, C/C++) that are available in a DLL (we are talking about the Windows platform here). It could be because you wish to re-use some code that you have earlier written in C/C++, or you might be calling a 3rd party library that provides some functionality you need at this point. OK, how easy is this process?

As it turns out, this is not difficult at all. In this article, I will show the steps to get this integration working.

First, my development environment:

1) Visual Studio Professional 2022 (64 bit) 17.1.0 on Windows 10.

2) Rust 1.59.0 running on Windows 10 (64 bit)

3) CLion 2021.3.4 IDE with Rust Plugin

Building the DLL and LIB

Although I could have assumed that we already have the DLL and corresponding LIB, let me show you how to create a simple DLL from scratch using Visual Studio 2022.

1) Launch VS 2022. Select the option to “Create a New Project” and choose the type as “DLL”. Click “Next”.

New DLL Project

New DLL Project

2) Then specify the Project Name and Location and click “Create”:

Project Name and Location

Project Name and Location

3) This will result in the auto generation of project files, including a bare-bones “dllmain.cpp” file:

Generated Code

Generated Code

Let us delete the generated function and type in our function, which will be called from Rust:

Our Exported Function

Our Exported Function

The extra qualifiers before the function reurn type ensure that this will be exported from the DLL without any name mangling.

4) Make sure the target is “x64”. Additionally, for our example, we will choose “Release” mode.

5) Build the project. This will result in the files “DllExample.dll” and “DllExample.lib” being created in “x64\Release” sub-directory within the project directory.

We need the above two files when we build our Rust project in CLion.

Rust Example that Uses the DLL

We now launch CLion and create a New Rust project. Here is the actual Rust code we will be using:

Rust Code

Rust Code

Since we are making a call to an external function, we have to declare the equivalent function signature in Rust, and also specify the external library to use. Notice that we are indicating the “kind” as “static” since we will be linking the Rust code with our DllExample.lib

Another important thing to note in the above code is the use of “unsafe” block. This is required since an external (non-Rust) code might have “unsafe” behavior.

Before we build the Rust project, we need to copy our DllExample.lib file to a location that can be configured in the “LIB” path. This is what I did:

a) Copied the LIB file to the Rust project directory. Here are the files in this directory:

LIB File Directory

LIB File Directory

Then I updated the “LIB” environment variable to include the path: “F:\Rust Projects\CallingDLL\”

b) Copied the DllExample.dll file to the “debug” subdirectory under “target” directory:

Executable and DLL

Executable and DLL

This is required in order to run the final Executable file.

We can now build and run the Rust project. 

Here is the program output as it appears within CLion IDE:

Program Output

Program Output

This shows that our external function in the DLL is called from within Rust. You can download the relevant files from here.

That was straightforward, right? You may find this article useful if you would like to explore “Foreign Function Interface (FFI)” of Rust in greater detail.

Have a nice week!

我一开始很纳闷链接的时候依然使用的是static呀,这样真的是动态链接吗.

真的是动态链接,以下是做的实验

  • 把target目录下的DllExample.dll移除再运行,运行失败.移回,再运行,运行成功.
  • 按照它的dll制作方法制作一份新的DllExample.dll内部函数为 extern "C" _declspec(dllexport) int fooBar(int arg) { return 9;}删除target目录下的原dll,更换自己的新dll.rust文件不进行重译,直接运行,可以看到输出的结果为9.的确使用了新的dll.

经过查找资料最后知道vistul studio可以编译两种.lib文件.一种是纯静态链接的.lib文件.另一种叫导入库(import lib).两者的区别是出静态链接.lib文件内部包含了需要的函数代码,而导入库(import lib)内部并没有包含代码,只是一些占位符,描述了各个函数所在.dll文件的位置.具体可以参考lib文件和dll文件icon-default.png?t=N7T8https://www.jianshu.com/p/33aa9e6c7b37
动态链接库DLL和静态链接库Libicon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/37918581

参考资料 

Rust Toolchain Platform/Target Questionsicon-default.png?t=N7T8https://users.rust-lang.org/t/rust-toolchain-platform-target-questions/88118 ABI的调用约定,类型表示和名称修饰icon-default.png?t=N7T8http://t.csdnimg.cn/KHJ0hRust 实战练习 - 7. FFI, 库, ABI, libcicon-default.png?t=N7T8http://t.csdnimg.cn/FnL6K

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值