外部函数接口

介绍

本指南将使用snappy压缩/解压库来介绍如何为外部代码编写绑定,Rust目前还不能直接调用c++库,但是snappy包含了一个C接口(详见snappy-c.h)。
(目前不太了解什么叫做编写绑定?)

关于libc的说明

其中许多示例使用libc crate,它为C类型提供了各种类型定义。如果你正在尝试这些例子,你需要将libc添加到你的Cargo.toml:

[dependencies]
libc = “0.2.0”

libc提供了在Rust支持的每个平台上轻松地与C代码(或“类似于C的”代码)进行互操作所需的所有定义。这包括类型定义(例如c_int)、常量(例如EINVAL)以及函数头文件(例如malloc)。
这个板条箱导出了板条箱根目录下的所有底层平台类型、函数和常量,因此所有项都可以通过libc::foo访问。所有导出api的类型和值都与libc为之编译的平台匹配。

调用外部函数

下面是一个调用外部函数的最小示例,如果安装了snappy,它将编译:

extern crate libc;
use libc::size_t;#[link(name = "snappy")]
extern {fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}fn main() {let x = unsafe { snappy_max_compressed_length(100) };println!("max compressed length of a 100 byte buffer: {}", x);
}

extern模块是外部库中的函数签名列表,可以看到上边的extern模块中只有一个函数签名,snappy_max_compressed_length(),在这个例子中用的是c语言的接口,
#link[(…)]这个属性用于指示链接器链接snappy库,以便解析符号。

外部函数被认为是不安全的,因此对它们的调用需要用unsafe{}来包装,以此向编译器承诺其中包含的一切都是安全的。C库经常暴露不是线程安全的接口,几乎所有接受指针参数的函数对所有可能的输入都无效,因为指针可能是悬空的,而原始指针属于Rust的安全内存模型之外。

当向外部函数声明实参类型时,Rust编译器无法检查声明是否正确,因此正确地指定它是在运行时保持绑定正确的一部分。(意思是说,外部函数的参数类型指定rust不管,要自己保证他是否是正确的)

extern块可以扩展到覆盖整个snappy的API:

extern crate libc;
use libc::{c_int, size_t};#[link(name = "snappy")]
extern {fn snappy_compress(input: *const u8,input_length: size_t,compressed: *mut u8,compressed_length: *mut size_t) -> c_int;fn snappy_uncompress(compressed: *const u8,compressed_length: size_t,uncompressed: *mut u8,uncompressed_length: *mut size_t) -> c_int;fn snappy_max_compressed_length(source_length: size_t) -> size_t;fn snappy_uncompressed_length(compressed: *const u8,compressed_length: size_t,result: *mut size_t) -> c_int;fn snappy_validate_compressed_buffer(compressed: *const u8,compressed_length: size_t) -> c_int;
}

创建安全的接口

需要封装原始的C API以提供内存安全性,并使用向量等高级概念。库可以选择只公开安全的高级接口,而隐藏不安全的内部细节。

封装需要缓冲区的函数,需要使用slice::raw模块来让Rust vector成为指向内存的指针。Rust的向量被保证是一个连续的内存块。长度是当前包含的元素的数量,容量是已分配内存中元素的总大小。长度小于或等于容量。

pub fn validate_compressed_buffer(src: &[u8]) -> bool {unsafe {snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0}
}

上面的validate_compressed_buffer封装使用了一个不安全的块,但是它通过从函数签名中去掉不安全的元素来保证对所有输入调用它是安全的。

snappy_compress和snappy_uncompress函数更复杂,因为还必须分配一个缓冲区来保存输出。

snappy_max_compressed_length函数可用于分配一个具有容纳压缩输出所需的最大容量的向量。然后可以将向量作为输出参数传递给snappy_compress函数。还传递一个输出参数,用于在压缩后检索设置长度的真实长度。

pub fn compress(src: &[u8]) -> Vec<u8> {unsafe {let srclen = src.len() as size_t;let psrc = src.as_ptr();let mut dstlen = snappy_max_compressed_length(srclen);let mut dst = Vec::with_capacity(dstlen as usize);let pdst = dst.as_mut_ptr();snappy_compress(psrc, srclen, pdst, &mut dstlen);dst.set_len(dstlen as usize);dst}
}

解压缩也是类似的,因为snappy将未压缩的大小存储为压缩格式的一部分,而snappy_uncompressed_length将检索所需的精确缓冲区大小。

pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {unsafe {let srclen = src.len() as size_t;let psrc = src.as_ptr();let mut dstlen: size_t = 0;snappy_uncompressed_length(psrc, srclen, &mut dstlen);let mut dst = Vec::with_capacity(dstlen as usize);let pdst = dst.as_mut_ptr();if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {dst.set_len(dstlen as usize);Some(dst)} else {None // SNAPPY_INVALID_INPUT}}
}

析构函数

外部库经常将资源的所有权交给调用代码。当这种情况发生时,我们必须使用Rust的析构函数来提供安全和保证这些资源的释放(特别是在恐慌的情况下)。

从c代码回调到rust函数

一些外部库需要使用回调来向调用者报告它们的当前状态或中间数据。可以将Rust中定义的函数传递给外部库。这样做的要求是,用正确的调用约定将回调函数标记为extern,以使它可以从C代码中调用。

然后,回调函数可以通过注册调用发送到C库,然后从那里调用。

这块说白了就是要在c中调用rust函数需要怎么做

上例子:

rust code:
extern fn callback(a: i32) {println!("I'm called from C with value {0}", a);
}#[link(name = "extlib")]
extern {fn register_callback(cb: extern fn(i32)) -> i32;fn trigger_callback();
}fn main() {unsafe {register_callback(callback);trigger_callback(); // Triggers the callback.}
}
c code:
typedef void (*rust_callback)(int32_t);
rust_callback cb;int32_t register_callback(rust_callback callback) {cb = callback;return 1;
}void trigger_callback() {cb(7); // Will call callback(7) in Rust.
}

在这个例子中,Rust的main()会在C中调用trigger_callback(),而trigger_callback()又会回调到Rust中的callback()。

针对rust对象的回调

前一个例子展示了如何从C代码中调用全局函数。然而,通常希望回调的目标是一个特殊的Rust对象。这可以是代表各自C对象的封装器的对象。

这可以通过将对象的原始指针向下传递给C库来实现。然后,C库可以在通知中包含指向Rust对象的指针。这将允许回调不安全地访问被引用的Rust对象。

rust 代码
struct RustObject {a: i32,// Other members...
}extern "C" fn callback(target: *mut RustObject, a: i32) {println!("I'm called from C with value {0}", a);unsafe {// Update the value in RustObject with the value received from the callback:(*target).a = a;}
}#[link(name = "extlib")]
extern {fn register_callback(target: *mut RustObject,cb: extern fn(*mut RustObject, i32)) -> i32;fn trigger_callback();
}fn main() {// Create the object that will be referenced in the callback:let mut rust_object = Box::new(RustObject { a: 5 });unsafe {register_callback(&mut *rust_object, callback);trigger_callback();}
}
C 代码
typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;int32_t register_callback(void* callback_target, rust_callback callback) {cb_target = callback_target;cb = callback;return 1;
}void trigger_callback() {cb(cb_target, 7); // Will call callback(&rustObject, 7) in Rust.
}

异步回调

在前面给出的示例中,调用回调函数是作为对外部C库的函数调用的直接反应。为了回调的执行,当前线程的控制从Rust切换到C到Rust,但是最后回调是在调用触发回调的函数的同一个线程上执行的。

当外部库生成它自己的线程并从那里调用回调时(不同线程所以是异步),事情会变得更加复杂。在这种情况下,在回调中访问Rust数据结构尤其不安全,必须使用适当的同步机制。除了互斥锁等经典的同步机制外,Rust中的一种可能是使用通道(在std::sync::mpsc中)将调用回调的C线程的数据转发给Rust线程。

如果异步回调的目标是Rust地址空间中的一个特殊对象,那么在相应的Rust对象被销毁后,C库也绝对有必要不再执行回调。这可以通过在对象的析构函数中取消回调的注册,并以确保注销后不执行回调的方式设计库来实现。

Linking

extern块上的link属性提供了基本的构建块,用于指导rustc如何链接到本机库。现在有两种被接受的link属性形式:

#[link(name = "foo")]
#[link(name = "foo", kind = "bar")]

在这两种情况下,foo是我们要链接的本地库的名称,而在第二种情况下bar是编译器要链接的本地库的类型。目前已知的本机库有三种类型:

Dynamic - #[link(name = "readline")]
Static - #[link(name = "my_build_dependency", kind = "static")]
Frameworks - #[link(name = "CoreFoundation", kind = "framework")]

不同类型的值意在区分本机库如何参与链接。从链接的角度来看,Rust编译器创建了两种类型的工件:partial (rlib/staticlib)和final (dylib/binary)。本地动态库和框架依赖关系被传播到最终的工件边界,而静态库依赖关系根本不会传播,因为静态库直接集成到后续工件中。

关于如何使用这个模型的例子有:

本地构建依赖项。有时候在编写一些Rust代码时需要一些C/ c++粘合剂,但是以库格式分发C/ c++代码是一个负担。在这种情况下,代码将被归档到libfoo中。然后Rust箱子会通过#[link(name = “foo”, kind = “static”)]声明一个依赖项。
无论crate的输出是什么风格,本机静态库都将包含在输出中,这意味着不需要分发本机静态库。

正常的动态依赖关系。许多系统上都有通用的系统库(如readline),通常无法找到这些库的静态副本。当这个依赖被包含在Rust crate中时,部分目标(比如rlibs)将不会链接到库,但当rlib被包含在最终目标中时(比如二进制文件),本地库将会被链接进去。

Unsafe blocks

一些操作,如对原始指针进行解引用或调用标记为不安全的函数,只允许在不安全的块中进行。不安全块隔离了不安全,并向编译器承诺不安全块不会泄漏。

另一方面,不安全的函数会向全世界宣传它。一个不安全函数是这样写的:

unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }

此函数只能从不安全的块或其他不安全的函数中调用。

访问外部全局

外部api通常会导出一个全局变量,它可以做一些类似于跟踪全局状态的事情。为了访问这些变量,你可以在extern块中用static关键字声明它们:

extern crate libc;#[link(name = "readline")]
extern {static rl_readline_version: libc::c_int;
}fn main() {println!("You have readline version {} installed.",unsafe { rl_readline_version as i32 });
}

或者,您可能需要更改外部接口提供的全局状态。为此,可以使用mut声明静态变量,这样我们就可以对它们进行修改。

extern crate libc;use std::ffi::CString;
use std::ptr;#[link(name = "readline")]
extern {static mut rl_prompt: *const libc::c_char;
}fn main() {let prompt = CString::new("[my-awesome-shell] $").unwrap();unsafe {rl_prompt = prompt.as_ptr();println!("{:?}", rl_prompt);rl_prompt = ptr::null();}
}

请注意,与静态mut的所有交互都是不安全的,无论是读还是写。处理全局可变状态需要非常小心。

外部的调用约定

大多数外部代码都公开C ABI, Rust在调用外部函数时默认使用平台的C调用约定。一些外部函数,尤其是Windows API,使用其他调用约定。Rust提供了一种方法来告诉编译器使用哪种约定:

extern crate libc;#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}

这适用于整个extern块。支持的ABI约束列表如下:
stdcall
aapcs
cdecl
fastcall
vectorcall This is currently hidden behind the abi_vectorcall gate and is subject to change.
Rust
rust-intrinsic
system
C
win64
sysv64

这个列表中的大多数abis都是不言自明的,但是system abi可能看起来有点奇怪。这个约束选择合适的ABI来与目标库进行互操作。例如,在x86架构的win32上,这意味着使用的abi将是stdcall。然而,在x86_64上,windows使用C调用约定,因此将使用C。这意味着在前面的例子中,我们可以使用extern “system”{…}为所有 Windows系统定义一个块,而不仅仅是x86系统。

与外部代码的互操作性

只有当==#[repr©]属性被应用到结构体上时,Rust才能保证结构体的布局与C语言中的平台表示兼容。#[repr(C, packed)]可以用来在没有填充==的情况下布局结构体成员。#[repr©]也可以应用于枚举。

Rust拥有的box(Box)使用非空指针作为句柄,指向所包含的对象。但是,它们不应该手工创建,因为它们是由内部分配器管理的。可以安全地假定引用是直接指向该类型的不可空指针。然而,违反借用检查或可变规则并不能保证安全,所以如果需要的话,最好使用原始指针(*),因为编译器不能对它们做很多假设。(意思是如果有特殊的要求最好使用原始指针,因为编译器拿它没办法)

向量和字符串共享相同的基本内存布局,在vec和str模块中可以使用实用工具来使用C api。但是,字符串不会以\0结束。如果您需要一个以null结尾的字符串来实现与C语言的互操作性,那么您应该在std::ffi模块中使用CString类型。

libc板条箱包含了libc模块中C标准库的类型别名和函数定义,并且默认情况下Rust会链接到libc和libm。

可变的函数

在C语言中,函数可以是可变参数,这意味着它们接受可变数量的参数。这可以在Rust中通过指定…在外部函数声明的参数列表中:

extern {fn foo(x: i32, ...);
}fn main() {unsafe {foo(10, 20, 30, 40, 50);}
}

正常的Rust函数不能是可变的:


// This will not compilefn foo(x: i32, ...) {}

可空指针优化

某些Rust类型被定义为永不为空。这包括引用(&T, &mut), Box (Box),和函数指针(extern “abi” fn())。在与C接口时,经常使用可能为空的指针,这似乎需要一些混乱的转换和/或不安全的代码来处理与Rust类型的转换。然而,该语言提供了一个解决方案。

作为一种特殊情况,如果枚举恰好包含两个变量(一个不包含数据,另一个包含上面列出的非空类型之一的字段),则该枚举符合“可空指针优化”。这意味着判别器不需要额外的空间;相反,空变量是通过将空值放入非空字段来表示的。这被称为“优化”,但与其他优化不同的是,它保证适用于符合条件的类型。

最常见的利用可空指针优化的类型是Option,其中None对应于null。所以Option<extern “C” fn(c_int) -> c_int>是使用C ABI(对应于C类型int (*)(int))表示可空函数指针的正确方法。

这里有一个人为的例子。假设某个C库有一个用于注册回调的工具,它在某些情况下会被调用。回调函数被传递一个函数指针和一个整型数,它应该以整型数作为参数运行函数。所以我们有函数指针在两个方向上飞过FFI边界。

extern crate libc;
use libc::c_int;extern "C" {/// Registers the callback.fn register(cb: Option<extern "C" fn(Option<extern "C" fn(c_int) -> c_int>, c_int) -> c_int>);
}/// This fairly useless function receives a function pointer and an integer
/// from C, and returns the result of calling the function with the integer.
/// In case no function is provided, it squares the integer by default.
extern "C" fn apply(process: Option<extern "C" fn(c_int) -> c_int>, int: c_int) -> c_int {match process {Some(f) => f(int),None    => int * int}
}fn main() {unsafe {register(Some(apply));}
}

从c中调用rust代码

你可能希望以一种可以从c中调用Rust代码的方式来编译它,这是相当容易的,但需要一些事情:

#[no_mangle]
pub extern "C" fn hello_rust() -> *const u8 {"Hello, world!\0".as_ptr()
}

extern“C”使该函数遵守C调用约定,如前面“外部调用约定”中讨论的那样。no_mangle属性关闭Rust的名称mangling,以便更容易链接到它。

FFI和panic

警惕panic是很重要的!在与FFI合作时。一个panic!跨越FFI边界是未定义的行为。如果你写的代码可能会引起恐慌,你应该在一个闭包中用catch_unwind来运行它:

use std::panic::catch_unwind;#[no_mangle]
pub extern fn oh_no() -> i32 {let result = catch_unwind(|| {panic!("Oops!");});match result {Ok(_) => 0,Err(_) => 1,}
}fn main() {}

请注意,catch_unwind只会捕捉unwind恐慌,而不会捕捉中止进程的恐慌。有关更多信息,请参阅catch_unwind的文档。

表示不透明的结构

有时,C库希望提供指向某个对象的指针,但不让您知道它想要的对象的内部细节。一个稳定而简单的方法是使用void *参数:

void foo(void *arg);
void bar(void *arg);

我们可以在Rust中用c_void类型表示:

extern crate libc;extern "C" {pub fn foo(arg: *mut libc::c_void);pub fn bar(arg: *mut libc::c_void);
}

这是处理这种情况的完全有效的方法。不过,我们可以做得更好一点。为了解决这个问题,一些C库会创建一个结构体,其中结构体的细节和内存布局是私有的。这提供了一定程度的类型安全。这些结构被称为“不透明”。这里有一个例子,在C语言中:

struct Foo; /* Foo is a structure, but its contents are not part of the public interface */
struct Bar;
void foo(struct Foo *arg);
void bar(struct Bar *arg);

为了在Rust中做到这一点,让我们创建自己的opaque类型:

#[repr(C)]
pub struct Foo {_data: [u8; 0],_marker:core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}
#[repr(C)]
pub struct Bar {_data: [u8; 0],_marker:core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}extern "C" {pub fn foo(arg: *mut Foo);pub fn bar(arg: *mut Bar);
}

通过包含至少一个私有字段而不包含构造函数,我们创建了一个无法在模块外部实例化的不透明类型。(没有字段的结构体可以被任何人实例化。)我们也想在FFI中使用这种类型,所以我们必须添加#[repr©]。该标记确保编译器不会将该结构标记为Send, Sync和Unpin不应用于该结构。(*mut u8不是发送或同步,phantompins不是Unpin)

但是因为我们的Foo和Bar类型不同,我们将在它们之间获得类型安全,所以我们不能意外地将一个指向Foo的指针传递给Bar()。

请注意,使用空枚举作为FFI类型是一个非常糟糕的主意。编译器依赖于空枚举是无人居住的,所以处理&Empty类型的值是一个巨大的脚注,可能会导致程序的错误行为(通过触发未定义的行为)。

rust ffi理解相关推荐

  1. Rust FFI 编程--理解不同语言的数据类型转换

    1. 简介 "FFI"是" Foreign Function Interface"的缩写,大意为不同编程语言所写程序间的相互调用.鉴于C语言事实上是编程语言界的 ...

  2. Rust FFI 编程 - Bindgen 工具介绍

    前面我们经历了<Rust FFI 编程 - 基础知识>.<Rust FFI 编程 - 手动绑定 C 库>和<Rust FFI 编程 - Rust 导出共享库>三个大 ...

  3. Rust FFI 与C语言互相调用

    Rust FFI 与C语言互相调用 参考 cbindgen 简介 二进制方式构建 脚本构建 Demo程序说明 示例工程: makefile test脚本 基本数据类型 Rust侧 C侧 对象 Rust ...

  4. Rust FFI 编程 - bindgen 使用示例

    当我们拥有一组具有良好声明的头文件时,自己定义 C 库的 Rust FFI 绑定函数是毫无意义的.我们可以使用 bindgen 这种工具从 C 库的头文件生成 Rust FFI 绑定函数.然后,我们运 ...

  5. rust编程-rust所有权理解(chapter 4.2 引用实质是借用)

    目录 2. 引用与借用 2.1 可变(mutable)引用 2.2 悬空(dangling)引用 2.3 引用的规则总结 2. 引用与借用 上一章节中提到,所有权在函数调用中的转移,函数返回必须同时返 ...

  6. 查看so库中是否有某个定义_从 Rust 库中公开 FFI

    Wikipedia 将 FFI 定义为一种机制,通过这种机制,用一种编程语言编写的程序可以调用或使用用另一种编程语言编写的服务. FFI 可用于加快程序执行(这在 Python 或 Ruby 这类动态 ...

  7. Rust的前景怎么样?值不值的学—Rust对比、特色和理念

    前言 其实我一直弄不明白一点,那就是计算机技术的发展,是让这个世界变得简单了,还是变得更复杂了. 当然这只是一个玩笑,可别把这个问题当真. 然而对于IT从业者来说,这可不是一个玩笑.几乎每一次的技术发 ...

  8. FinClip小程序+Rust(三):一个加密钱包

    一个加密货币钱包,主要依赖加密算法构建.这部分逻辑无关iOS还是Android,特别适合用Rust去实现.我们看看如何实现一个生成一个模拟钱包,准备供小程序开发采用 前言 在之前的内容我们介绍了整个端 ...

  9. rust php 扩展,在PHP程序中使用Rust扩展的方法_PHP

    C或PHP中的Rust 我的基本出发点就是写一些可以编译的Rust代码到一个库里面,并写为它一些C的头文件,在C中为被调用的PHP做一个拓展.虽然并不是很简单,但是很有趣. Rust FFI(fore ...

  10. rust php 扩展,在PHP程序中使用Rust扩展的方法_php技巧

    C或PHP中的Rust 我的基本出发点就是写一些可以编译的Rust代码到一个库里面,并写为它一些C的头文件,在C中为被调用的PHP做一个拓展.虽然并不是很简单,但是很有趣. Rust FFI(fore ...

最新文章

  1. 为什么分布式一定要有延时任务?
  2. POJ 1679 判断最小树是否唯一
  3. 用HttpURLConnection发送http请求
  4. 行内元素中间出现空隙
  5. linux iis 环境配置教程,Linux系统Java环境配置教程
  6. SpringBoot 优雅的配置拦截器方式
  7. 谷歌正式推出在线云储存服务Google Drive
  8. 制作app怎么连接服务器,App制作步骤、流程有哪些?
  9. 图像分类以及经典的分类模型
  10. 阿里云对象存储服务OSS前后联调
  11. ubuntu 18.04取消自动锁屏功能
  12. 认真学习MySQL中的角色权限控制
  13. 山东建筑大学计算机学院孙倩,山东建筑大学毕业设计答辩.pdf
  14. 给电脑重装系统的方法与重装过程中问题解决
  15. 华硕B85M-V PLUS 刷NVME协议BIOS
  16. 2018秋招校招济南联通软件研究院笔试题目
  17. java 记录错误日志文件_java日志记录错误的文件_方法_行号_报错信息
  18. C语言简单实现计算一组数据中奇偶数个数
  19. MyBatis-Plus(基于springboot)直接上手
  20. trim有什么用计算机二级,玩转SSD中的TRIM技术

热门文章

  1. DOS 之for循环
  2. Back键失效的原因分析及解决(基于Android 4.4.3源码分析)
  3. Edge浏览器运行卡顿怎么办 怎样让Edge浏览器速度更快
  4. 举个栗子!Tableau技巧(61):学做三个集合的维恩图(文氏图)Venn diagram
  5. 使用浏览器访问或调试微信公众号(跳过微信认证)
  6. phyton题库+解析
  7. mysql星期几转为英文_MySQL如何获取一个指定日期所对应的的星期几(英文星期信息-dayname函数)呢?...
  8. 产品经理的私房菜 - 腾讯产品模型 - 执行力篇
  9. 【暴强】200种好口碑便宜护肤品 - 健康程序员,至尚生活!
  10. 杭电 1242 Rescue