Rust FFI 编程 - Rust导出共享库02
这一篇我们来探讨Rust导出共享库时如何传递字符串,主要涉及字符串作为函数参数和函数返回值的处理细节。我们首先回顾关于字符串的基础知识,了解其在Rust和C中的区别,然后设计具体的示例进行实践,并整理出传递字符串的FFI编程范式。
基础知识
在 C 语言中,字符串可看作是由字符组成的一维的字节数组。但在内存中具体如何保存每个字符,这依赖于特定的字符编码。字符串常量默认是以 NUL 字符结尾,通常用转义序列' '
表示,由 C 编译器自动添加。
字符串可以用指针和字节数组来表示,这是两种不同方式的存储:
将字符串存储在字符类型的数组中时,最初,字符串是字节序列,其中每个字节代表一个字符。但后来为了表示宽字符,ISO C 标准引入了新类型。一般,char
表示ASCII和UTF-8编码,wchar_t
表示UTF-16等“宽”字符编码。
大多数字符串和I/O库函数都采用char *
参数,该参数表示指向字符串中的第一个字符(即存储该字符串的数组的第一个元素)。由于传递给函数的是第一个元素的地址,因此该函数并不知道数组有多大,只能依靠空终止符来判断何时停止处理。
1)共享的只读字符串 char *
。在大多数编译器中,将字符串字面量直接分配给指针后,字符串常量被存储于初始化数据段的只读(.roadata)区域,而指针变量被存储于读写区域中,也就是说可以更改指针以指向其它内容,但不能更改字符串常量的内容。因此,仅当不需要在程序的后期修改字符串时,应使用char *
方式声明。
2)动态分配的可变字符串 char []
。将字符串对字节数组进行初始化后,在函数执行时会被拷贝到栈区或堆区(使用 malloc
),这时数组的内容是可以被修改的。因此,对于需要修改的字符串,应使用char[]
方式声明。同时由于 C 指针是一个用数值表示的地址,因此,可以对指针执行算术运算来修改字符串。
代码示例如下:
// ffi/example_01/csrc/hello.c
// basic string - char pointer
char *str;
str = "hello"; // Stored in read only part of data segment
*(str+1) = 'i'; // Segmentation fault error: trying to modify read only memory
char hello_s[] = "hello"; // Stored in stack segment
*(hello_s+0) = 'H'; // No problem: String is now Hello
printf("new string is %sn", hello_s);
在 Rust 语言中,字符串是由字符的 UTF-8 编码组成的字节序列。出于内存安全的考虑,字符串被分为了很多种类型来表示,我们来看一张图。
我们简单介绍以下几个类型,其余类型可以看 Rust 标准库的文档。
-
str
:这是 Rust 语言核心中仅有的一种字符串类型,Rust 标准库中提供了其它的字符串类型。 -
&str
:表示不可变的 UTF-8 编码的字节序列,它是str
类型的引用属于引用类型; -
String
:表示可变的字符串,拥有所有权,其本质是一个成员变量是Vec<u8>
类型的结构体; -
CStr
:表示以空字符终止的 C 字符串或字节数组的借用,属于引用类型。一般用于和 C 语言交互,由 C 分配并被 Rust 借用的字符串; -
CString
:表示拥有所有权的,中间没有空字节,以空字符终止的字符串类型。一般用于和 C 语言交互时,由 Rust 分配并传递给 C 的字符串;
除此之外,从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇( 字母 的概念)。在 Rust 标准库中提供了对字符串按字符处理(chars())和按字节(bytes())处理的操作支持,其中单个字符是用char
类型来表示,而使用u8
来表示字节类型。注意:定义字符是使用单引号,用双引号定义的是字符串常量。
我们可以看到 Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式。Rust 相比其他语言更多的暴露出了字符串的复杂性,这种权衡取舍使的程序员在开发中免于处理涉及非 ASCII 字符的错误。
示例实践
了解完这些基础知识后,我们设计示例来展示字符串作为函数参数和函数返回值的处理细节。
- 有
print_str
和change_str
两个函数,其参数均为 C 端生成的一个字符串,分别实现打印和修改该字符串的功能; - 有个
generate_str
函数,其返回值是 Rust 端生成的一个字符串,以及free_str
函数供 C 端调用者将字符串返回给 Rust 释放内存;
头文件如下:
void print_str(char *str);
char *change_str(char str[]);
char *generate_str();
void free_str(char *);
Rust 共享库的实现如下:
use std::os::raw::c_char;
use std::ffi::{CStr, CString};
#[no_mangle]
pub extern "C" fn print_str(s: *const c_char) {
let slice = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s)
};
let r_str = slice.to_str().unwrap();
println!("Rust side print: {:?}", r_str);
}
#[no_mangle]
pub extern "C" fn change_str(s: *mut c_char) -> *mut c_char {
let mut string = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s).to_string_lossy().into_owned()
};
string.push_str(" World!");
println!("Rust side change: {:?}", string);
let c_str_changed = CString::new(string).unwrap();
c_str_changed.into_raw()
}
#[no_mangle]
pub extern "C" fn generate_str() -> *mut c_char {
let ping = String::from("ping");
println!("Rust side generate: {:?}", ping);
let c_str_ping = CString::new(ping).unwrap();
c_str_ping.into_raw()
}
#[no_mangle]
pub extern "C" fn free_str(s: *mut c_char) {
unsafe {
if s.is_null() {
return;
}
CString::from_raw(s)
};
}
我们可以总结出在 Rust 和 C 之间传递字符串的编程范式。
- 使用
std::ffi::CStr
提供的from_ptr
方法包装 C 的字符串指针,它基于空字符' '
来计算字符串的长度,并可以通过它将外部 C 字符串转换为 Rust 的&str
和String
。 - 使用
std::ffi::CString
提供的一对方法into_raw
和from_raw
可以进行原始指针转换,由于将字符串的所有权转移给了调用者,所以调用者必须将字符串返回给 Rust,以便正确地释放内存。 - 我们必须确保 C 中的字符串是有效的UTF-8编码,且引用字符串的指针不能为 NULL,因为 Rust 的引用不允许为 NULL。
完整代码:https://github.com/lesterli/rust-practice/tree/master/ffi/example_01
后记
出于严谨考虑,示例代码我用 valgrind 工具做了个内存泄露分析,发现虽然没有错误,但显示有个“still reachable: 1,200 bytes in 7 blocks”类型的泄露,我加上 --show-reachable=yes
选项进行定位,发现均发生在 C 端调用 Rust 的 print_str
函数处。谷歌找了半天原因,最终发现原来是跟 Rust 的行缓冲区 stdout
有关。
Rust 为了进行缓冲,它会分配一个静态的vec
,它只执行一次,每次调用时重用现有缓冲区。因为我们此处是从 C 端运行,并不能控制其 main
函数,因此它将不会被释放,这就是 valgrind 报告的原因所在。我们知道只是打印字符串到控制台,所以这个泄露不用太担心。
- 第四课:模型的使用
- 【Java概念学习】--数组的初始化
- linux下重命名文件或文件夹使用mv既可实现。
- 第三课:把tensorflow,模型和测试数据导入Android工程
- D-Link 路由器信息泄露和远程命令执行漏洞分析及全球数据分析报告
- Wordpress安全架构分析
- CVE-2017-5123 漏洞利用全攻略
- 简单分析shared pool(三) (r5笔记第94天)
- OpenCV在车道线查找中的使用
- ESP32 DevKitC 编译烧写 AliOS Things
- 使用R完成K近邻分类
- 使用R完成逻辑斯蒂回归分类 直接上代码,如下:
- 基于时间点的不完全恢复的例子(r6笔记第9天)
- R-正太分布,检验
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 小书MybatisPlus第3篇-自定义SQL
- Nginx + Spring Boot 实现负载均衡
- 小书MybatisPlus第2篇-条件构造器的应用及总结
- 一个案例演示 Spring Security 中粒度超细的权限控制!
- 信息收集之主机发现:nmap
- 文本文件逐行处理–用java8 Stream流的方式
- 使用java8API遍历过滤文件目录及子目录及隐藏文件
- 使用位运算、值交换等方式反转java字符串-共四种方法
- 精讲RestTemplate第2篇-多种底层HTTP客户端类库的切换
- 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用
- 在图中添加多边形
- 设置坐标轴刻度的位置和样式
- OkHttp透明压缩,收获性能10倍,外加故障一枚
- 体验spring-boot-devtools热部署,流畅且不失强大
- 【每周一库】 simsearch - a simple and lightweight fuzzy search engine