Rust-FFI复杂参数传递处理方式2--字符和字符串类型

本文详细介绍了C语言和Rust语言中字符串的表示与内存管理,包括C语言中的字符串常量与可变字符串,以及Rust中的str、&str、String、CStr和CString等不同类型。重点阐述了在Rust和C之间如何安全地传递和修改字符串,以及内存的释放。通过示例代码展示了如何在Rust中使用CStr和CString与C进行交互,并确保内存安全。
摘要由CSDN通过智能技术生成
预先知识点

1.在C语言里,字符串是什么?
字符串可看作是由字符组成的一维的字节数组。但在内存中具体如何保存每个字符,这依赖于特定的字符编码。字符串常量默认是以 NULL字符结尾,通常用转义序列’\0’表示,由 C 编译器自动添加。
字符串可以用指针和字节数组来表示,这是两种不同方式的存储:
将字符串存储在字符类型的数组中时,最初,字符串是字节序列,其中每个字节代表一个字符。但后来为了表示宽字符,ISO C 标准引入了新类型。一般,char表示ASCII和UTF-8编码,wchar_t表示UTF-16等“宽”字符编码。
大多数字符串和I/O库函数都采用char * 参数,该参数表示指向字符串中的第一个字符(即存储该字符串的数组的第一个元素)。由于传递给函数的是第一个元素的地址,因此该函数并不知道数组有多大,只能依靠空终止符来判断何时停止处理。

1)共享的只读字符串 char * 。在大多数编译器中,将字符串字面量直接分配给指针后,字符串常量被存储于初始化数据段的只读(.roadata)区域,而指针变量被存储于读写区域中,也就是说可以更改指针以指向其它内容,但不能更改字符串常量的内容。因此,仅当不需要在程序的后期修改字符串时,应使用char *方式声明。

2)动态分配的可变字符串 char []。将字符串对字节数组进行初始化后,在函数执行时会被拷贝到栈区或堆区(使用 malloc),这时数组的内容是可以被修改的。因此,对于需要修改的字符串,应使用char[] 方式声明。同时由于 C 指针是一个用数值表示的地址,因此,可以对指针执行算术运算来修改字符串。
对于上面的两种格式,我们可以写出以下代码:

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 %s\n", hello_s);

2.在 Rust 语言中,字符串是由字符的 UTF-8 编码组成的字节序列。出于内存安全的考虑,字符串被分为了很多种类型来表示,这里列举出几种常用的:
1)str:这是 Rust 语言核心中仅有的一种字符串类型,Rust 标准库中提供了其它的字符串类型。
2)&str:表示不可变的 UTF-8 编码的字节序列,它是str类型的切片属于引用类型。
3)String:表示可变的字符串,拥有所有权,其本质是一个成员变量是Vec类型的结构体。
4)CStr:表示以空字符终止的 C 字符串或字节数组的借用,属于引用类型。一般用于和 C 语言交互,由 C 分配并被 Rust 借用的字符串。
5)CString:表示拥有所有权的,中间没有空字节,以空字符终止的字符串类型。一般用于和 C 语言交互时,由 Rust 分配并传递给 C 的字符串。
后两种是FFI的C语言和Rust交互时使用的,

CStr:

对于产生于c的字符串(如在c程序中使用malloc产生),rust使用CStr来表示,和str类型对应,表明我们并不拥有这个字符串。
Rust调用C函数

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

extern {
fn char_func() -> *mut c_char;
}

fn get_string() -> String {
unsafe {
let raw_string: *mut c_char = char_func();
let cstr = CStr::from_ptr(raw_string());
cstr.to_string_lossy().into_owned()
}
}

在这里get_string使用CStr::from_ptr从C的char*获取一个字符串,并且转化成了一个String。
注意to_string_lossy()的使用:因为在rust中一切字符都是采用utf8表示的而c不是,
因此如果要将c的字符串转换到rust字符串的话,需要检查是否都为有效utf-8字节。to_string_lossy将返回一个Cow类型,
即如果c字符串都为有效utf-8字节,则将其0开销地转换成一个&str类型,若不是,rust会将其拷贝一份并且将非法字节用U+FFFD填充。

CString

和CStr表示从C中来,rust不拥有归属权的字符串相反,CString表示由rust分配,Rust拥有所有权,可以进行修改,用以传给C程序的字符串。

use std::ffi::CString;
use std::os::raw::c_char;
extern {
fn my_printer(s: *const c_char);
}
let c_to_print = CString::new("Hello, world!").unwrap();
unsafe {
my_printer(c_to_print.as_ptr()); // 使用 as_ptr 将CString转化成char指针传给c函数
}

注意c字符串中并不能包含’\0’字节(因为’\0’用来表示c字符串的结束符),因此CString::new将返回一个Result,
如果输入有’\0’的话则为Error(NulError)。

复杂参数传递处理方式2–字符和字符串类型

通过上面的知识点,应该对字符串类型有了一个比较深刻的认识,下面来看看代码如何编写。
设计4个函数:
有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 gennerate_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)
    };
}

C语言的代码:

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include "example_01.h"   

int main(void) {
  // basic string - char array
  char hello1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
  printf("C hello 1: %s\n", hello1);
  char hello2[6] = "Hello";
  printf("C hello 2: %s\n", hello2);
  if (strcmp(hello1, hello2) ==0)
  {
    printf("hello 1 and 2 are equal\n");
  } else
  {
    printf("hello 1 and 2 are different\n");
  }
  
  // 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 = 'H';         // No problem: String is now Hello
  printf("new string in stack is %s\n", hello_s);
  
  int size = 6;
  char *hello_h = (char *)malloc(sizeof(char)*size); // Stored in heap segment
  *(hello_h+0) = 'h';  
  *(hello_h+1) = 'e';   
  *(hello_h+2) = 'l';
  *(hello_h+3) = 'l'; 
  *(hello_h+4) = 'o'; 
  *(hello_h+5) = '\0';       
  *(hello_h+0) = 'H';  // No problem: String is now Hello
  printf("new string in heap is: %s\n", hello_h);
  free(hello_h);

  char *c_str = "hello";  
  print_str(c_str); // Problem: still reachable
  c_str = change_str(c_str); // change the previous content
  printf("C side result: %s\n", c_str);
  free_str(c_str);

  // C generate strings
  char *c_hello = (char *)malloc(sizeof(char)*size);
  *(c_hello+0) = 'H';  
  *(c_hello+1) = 'e';   
  *(c_hello+2) = 'l';
  *(c_hello+3) = 'l'; 
  *(c_hello+4) = 'o'; 
  *(c_hello+5) = '\0';       
  printf("C side generate: %s\n", c_hello);
  print_str(c_hello);
  char *c_hello_world = change_str(c_hello);
  printf("C side result: %s\n", c_hello_world);
  free(c_hello);
  free_str(c_hello_world);

  char *c_ping = generate_str();
  printf("C side print: %s\n", c_ping);
  free_str(c_ping);
}

我们可以总结出在 Rust 和 C 之间传递字符串的编程范式。
使用std::ffi::CStr提供的from_ptr方法包装 C 的字符串指针,它基于空字符’\0’来计算字符串的长度,并可以通过它将外部 C 字符串转换为 Rust 的 &str和String。
使用std::ffi::CString提供的一对方法into_raw和from_raw可以进行原始指针转换,由于将字符串的所有权转移给了调用者,所以调用者必须将字符串返回给 Rust,以便正确地释放内存。
我们必须确保 C 中的字符串是有效的UTF-8编码,且引用字符串的指针不能为 NULL,因为 Rust 的引用不允许为 NULL。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值