一文搞懂 Rust 与 C 语言交互的实现方式

在当今软件开发领域,性能优化和代码安全性是开发速度和灵活的内存管理闻名者关注的核心问题。Rust语言以其强大的内存安全性和并发性能著称,而C语言则以其接近硬件的运行速度和灵活的内存管理闻名。

为了更好地结合两者的优点,Rust与C语言的互操作性成为了热门话题。通过Rust的FFI(Foreign Function Interface)机制,开发者可以在Rust代码中调用C语言编写的函数,反之亦然。

这种互操作不仅允许Rust项目复用现有的C/C++代码库,还能在保持Rust性能和安全性的同时,实现性能敏感部分的优化。随着Rust在系统编程领域的广泛应用,其与C语言的互操作性将为软件开发带来更多的可能性和创新机会。

由于用户态STD库代码会调用大量的C语言库函数,因此先要研究Rust如何与C语言互操作。Rust与C语言的互操作涉及类型匹配及函数调用语义实现。

1.C语言的类型适配

要实现Rust与C语言的交互,必须在Rust中定义对应于C语言的类型集合,当Rust调用C语言函数或C语言调用Rust函数时,变量的类型必须属于这些类型集合。这部分源代码在标准库中的路径如下:

/library/core/ffi/mod.rs  

C语言类型可以与Rust类型完整地一一映射。对这些映射源代码的分析如下:

//以下定义了C语言类型与Rust类型的关系,

//对所有C语言类型以“c_xxxx”来定义为Rust对应类型的别名,

//仅给出针对Linux的代码

type_alias! { "c_char.md", c_char = c_char_definition::c_char,

                     NonZero_c_char = c_char_definition::NonZero_c_char; }

type_alias! { "c_schar.md", c_schar = i8, NonZero_c_schar = NonZeroI8; }

type_alias! { "c_uchar.md", c_uchar = u8, NonZero_c_uchar = NonZeroU8; }

type_alias! { "c_short.md", c_short = i16, NonZero_c_short = NonZeroI16; }

type_alias! { "c_ushort.md", c_ushort = u16, NonZero_c_ushort = NonZeroU16; }

type_alias! { "c_int.md", c_int = i32, NonZero_c_int = NonZeroI32; }

type_alias! { "c_uint.md", c_uint = u32, NonZero_c_uint = NonZeroU32; }

type_alias! { "c_long.md", c_long = i32, NonZero_c_long = NonZeroI32;

type_alias! { "c_ulong.md", c_ulong = u32, NonZero_c_ulong = NonZeroU32;

type_alias! { "c_longlong.md", c_longlong = i64, NonZero_c_longlong = NonZeroI64; }

type_alias! { "c_ulonglong.md", c_ulonglong = u64, NonZero_c_ulonglong = NonZeroU64; }

type_alias_no_nz! { "c_float.md", c_float = f32; }

type_alias_no_nz! { "c_double.md", c_double = f64; }

//此宏用于定义C语言类型的Docfile

macro_rules! type_alias {

    {

      $Docfile:tt, $Alias:ident = $Real:ty, $NZAlias:ident = $NZReal:ty;

      $( $Cfg:tt )*

    } => {

        //不包含非零类型的定义宏

        type_alias_no_nz! { $Docfile, $Alias = $Real; $( $Cfg )* }

        #[doc = concat!("Type alias for 'NonZero' version of ['", stringify! ($Alias), "']")] //Docfile部分

        #[unstable(feature = "raw_os_nonzero", issue = "82363")]

        $( $Cfg )*

        pub type $NZAlias = $NZReal; //定义C语言的非零类型为Rust类型的别名

    }

}

//此宏不能定义NonZero类型

macro_rules! type_alias_no_nz {

    {

      $Docfile:tt, $Alias:ident = $Real:ty;

      $( $Cfg:tt )*

    } => {

        #[doc = include_str!($Docfile)] //定义Docfile

        $( $Cfg )*

        #[unstable(feature = "core_ffi_c", issue = "94501")]

        pub type $Alias = $Real; 

    }

}

//由于以下3个类型无法使用上面的宏进行定义,因此需要单独定义

pub type c_size_t = usize;

pub type c_ptrdiff_t = isize;

pub type c_ssize_t = isize;

//此宏用于定义char类型

mod c_char_definition {

    cfg_if! {

        if #[cfg(any(

            all(

                target_os = "linux",  //target_os指明代码运行的目标操作系统为Linux

                any(  //target_os指明了代码可以适配的CPU

                    target_arch = "aarch64",

                    target_arch = "arm",

                    target_arch = "powerpc",

                    target_arch = "powerpc64",

                    target_arch = "s390x",

                    target_arch = "riscv64",

                    target_arch = "riscv32"

                )

            ),

            all(target_os = "fuchsia", target_arch = "aarch64")

        ))] {

            pub type c_char = u8;

            pub type NonZero_c_char = crate::num::NonZeroU8;

        }

    }        

}

2.C语言的va_list类型适配

C语言的va_list是概念非常复杂的类型,而且与具体的OS、CPU架构、编译器紧密相关,Rust能够适配va_list类型,说明Rust与C语言同样具有强大的功能。本节列举基于x86_64且OS为Linux的示例,对相关源代码的分析如下:

//因为编译器需要,所以用以下enum来实现类似C语言的void类型

pub enum c_void {

    __variant1,

    __variant2,

}

//以下的类型与C语言的va_list类型在二进制上是兼容的,CPU架构是x86_64,OS是Linux

#[repr(C)]

pub struct VaListImpl<'f> {

    gp_offset: i32,

    fp_offset: i32,

    overflow_arg_area: *mut c_void, //栈空间用于存储可变参数变量

    reg_save_area: *mut c_void, //寄存器用于存储可变参数变量

    _marker: PhantomData<&'f mut &'f c_void>, //表明成员与结构体的生命周期关系

}

//Rust与va_list类型适配的类型是VaList

pub struct VaList<'a, 'f: 'a> {

    inner: &'a mut VaListImpl<'f>,

    _marker: PhantomData<&'a mut VaListImpl<'f>>,

}

impl<'f> VaListImpl<'f> {

    //此函数用于将C语言的va_list类型转换为Rust的VaList类型

    pub fn as_va_list<'a>(&'a mut self) -> VaList<'a, 'f> {

        VaList { inner: self, _marker: PhantomData }

    }

}

impl<'f> Clone for VaListImpl<'f> {

   //此函数可以实现C语言中va_copy()函数的功能

   fn clone(&self) -> Self {

        let mut dest = crate::mem::MaybeUninit::uninit();

        unsafe {

            va_copy(dest.as_mut_ptr(), self); //va_copy()函数由编译器固有函数实现

            dest.assume_init()

        }

    }

}

impl<'f> VaListImpl<'f> {

    //此函数可以实现C语言中va_arg()函数的功能

    pub unsafe fn arg(&mut self) -> T {

        unsafe {  va_arg(self)  } //比C语言的实现少了一个参数

    }

    //此函数利用闭包处理VaList

    pub unsafe fn with_copy(&self, f: F) -> R

    where

        F: for<'copy> FnOnce(VaList<'copy, 'f>) -> R,

    {

        let mut ap = self.clone();

        let ret = f(ap.as_va_list());

        unsafe {  va_end(&mut ap);  } //self已经被全部取出,需要调用va_end()函数结束

        ret

    }

}

extern "rust-intrinsic" {

    //以下代码中没有与C语言的va_start()函数对应的内容,因为Rust不需要这个功能

    fn va_end(ap: &mut VaListImpl<'_>); //清除va_list类型

    //将一个va_list类型完整复制

    fn va_copy<'f>(dest: *mut VaListImpl<'f>, src: &VaListImpl<'f>);  

    //获取下一个类型为T的arg

    fn va_arg(ap: &mut VaListImpl<'_>) -> T;  

}

进行va_list类型适配,Rust可以解析C语言函数中的va_list参数

3.C语言字符串类型适配

C语言字符串类型与Rust字符串类型在语法上是有区别的。C语言的字符串类型尾部成员必须为“\0”。Rust定义了CStr与CString来适配C语言字符串类型。CStr通常用于不可修改的字面量,而CString用于可修改的字符串。这两个类型的源代码在标准库中的路径如下:

/library/std/src/ffi/c_str.rs    

CStrCString不涉及迭代器、格式化、加减、分裂、字符查找等操作其主要功能是StrString增加尾部为0的字节以便能将字符串作为C语言的函数参数;C语言输入字符串用CStrCString完成安全封装后继可再转换为StrString。如果需要将Rust的Str、String转换为C语言的函数参数,则必须先把Rust中的字符串转换为CString,才能输出到C语言。对这两个类型定义源代码的分析如下:

pub struct CString {

    inner: box<[u8]>, //[u8]应该以0结尾

}

pub struct CStr {

    inner: [c_char], //切片最后一个成员应该是0

}

CStrC语言的字符串类型“char*”进行封装并定义转换函数C语言的字符串变量适配成安全的Rust类型变量并在需要时转化成Str类型。对CStr类型源代码的分析如下:

impl CStr {

    //首先此函数用于接收一个由C语言模块传递过来的char *指针,然后创建Rust的CStr引用并返回。

    //调用代码应该保证传入参数的正确性。此函数返回的引用生命周期由调用代码的上下文决定,

    //生命周期的正确性也由调用代码保证

    pub unsafe fn from_ptr<'a>(ptr: *const c_char) -> &'a CStr {

        //此代码块用于将* const c_char转换为 &[u8]

        unsafe {

            //调用C语言的库函数sys::strlen()来获取字符串长度

            let len = sys::strlen(ptr);

            let ptr = ptr as *const u8;

            //先创建&[u8],再创建Self类型引用

            Self::_from_bytes_with_nul_unchecked(slice::from_raw_parts(ptr, len as usize + 1))

        }

    }

    //此函数用于从准备好的[u8]中创建CStr的引用并返回,CStr的生命周期短于bytes的生命周期

    pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &[u8]) -> &CStr {

        //bytes的最后一个字节必须为0

        debug_assert!(!bytes.is_empty() && bytes[bytes.len() - 1] == 0);

        unsafe { Self::_from_bytes_with_nul_unchecked(bytes) }

    }

    const unsafe fn _from_bytes_with_nul_unchecked(bytes: &[u8]) -> &Self {

        //将bytes强制转换为CStr裸指针后解引用再取引用

        unsafe { &*(bytes as *const [u8] as *const Self) }

    }

    //此函数利用bytes构造CStr

    pub fn from_bytes_until_nul(bytes: &[u8]) -> Result<&CStr, FromBytesUntilNulError> {

        //memchr()函数用于查找为0的字节位置,即C语言字符串尾部位置

        let nul_pos = memchr::memchr(0, bytes);

        match nul_pos {

            Some(nul_pos) => {

                //在bytes中取出C语言的字符串子切片

                let subslice = &bytes[..nul_pos + 1];

                Ok(unsafe { CStr::from_bytes_with_nul_unchecked(subslice) })

            }

            None => Err(FromBytesUntilNulError(())),

        }

    }

    //此函数用于将CStr转换为C语言的字符串,但需要保证符合C语言字符串的规则。

    //此函数的不当使用会产生悬垂指针

    // use std::ffi::CString;

    // let ptr = CString::new("Hello").expect("CString::new failed").as_ptr();

    // unsafe { *ptr; } //这里会有悬垂指针,ptr指向的内存的生命周期已经终止

    //可使用如下函数

    // use std::ffi::CString;

    //hello的生命周期会到作用域尾部

    // let hello = CString::new("Hello").expect("CString::new failed");

    // let ptr = hello.as_ptr();

    // unsafe {  *ptr; } //此时不会有悬垂指针问题

    pub const fn as_ptr(&self) -> *const c_char {

        self.inner.as_ptr()

    }

    //此函数用于将self转换成去掉尾部0的[u8]切片引用后,此时内存仍然是从C语言传递过来的内存

    pub fn to_bytes(&self) -> &[u8] {

        let bytes = self.to_bytes_with_nul();

        unsafe { bytes.get_unchecked(..bytes.len() - 1) }

    }

    //此函数用于将self转换为[u8]切片引用后,尾部仍然有0

    pub fn to_bytes_with_nul(&self) -> &[u8] {

        unsafe { &*(&self.inner as *const [c_char] as *const [u8]) }

    }

    //此函数用于将self转换为&str后,仍然使用C语言传递过来的内存

    pub fn to_str(&self) -> Result<&str, str::Utf8Error> {

        str::from_utf8(self.to_bytes())

    }

    //此函数用于将堆内存中的CStr转换为CString后,内存已经被重新申请

    pub fn into_c_string(self: Box) -> CString {

        let raw = Box::into_raw(self) as *mut [u8]; //从Box中取出堆内存

        //重新形成Box结构,并创建CString

        CString { inner: unsafe { Box::from_raw(raw) } }

    }

}

在代码调用C语言的函数时,最好使用CString类型。因为有些C语言函数会默认字符串位于堆内存。对CString类型源代码的分析如下:

impl CString {

    //构造函数

    pub fn new>>(t: T) -> Result {

        trait SpecNewImpl {

            fn spec_new_impl(self) -> Result;

        }

        //对于可以转换为Vec的类型变量,使用此变量的堆内存构造String

        impl>> SpecNewImpl for T {

            default fn spec_new_impl(self) -> Result {

                let bytes: Vec = self.into(); //将bytes转换为Vec变量

                match memchr::memchr(0, &bytes) {

                    //bytes中不应该有值为0的字节存在

                    Some(i) => Err(NulError(i, bytes)),

                    None => Ok(unsafe { CString::_from_vec_unchecked(bytes) }),

                }

            }

        }

        //此函数利用字节切片构造String,会重新申请堆内存

        fn spec_new_impl_bytes(bytes: &[u8]) -> Result {

            //由于bytes中没有0,因此长度需要加1

            let capacity = bytes.len().checked_add(1).unwrap();

            let mut buffer = Vec::with_capacity(capacity); //构造Vec变量

            

            buffer.extend(bytes);//将bytes写入Vec变量,此时还没有给buffer的尾部赋0值

            

            match memchr::memchr(0, bytes) { //检测bytes内是否有0值

                //如果有0,则出错,将buffer及0的位置返回

                Some(i) => Err(NulError(i, buffer)),

                //如果无0,则生成自动填0的CString

                None => Ok(unsafe { CString::_from_vec_unchecked(buffer) }),

            }

        }

        //为&[u8]实现SpecNewImpl Trait,提高程序运行效率

        impl SpecNewImpl for &'_ [u8] {

            fn spec_new_impl(self) -> Result {

                spec_new_impl_bytes(self)

            }

        }

        //为&str实现SpecNewImpl Trait,提高程序运行效率

        impl SpecNewImpl for &'_ str {

            fn spec_new_impl(self) -> Result {

                spec_new_impl_bytes(self.as_bytes())

            }

        }

        //为&mut [u8]实现SpecNewImpl Trait,提高程序运行效率

        impl SpecNewImpl for &'_ mut [u8] {

            fn spec_new_impl(self) -> Result {

                spec_new_impl_bytes(self)

            }

        }

        //构造CString,如果t是&[u8]、&mut [u8]或&str类型的,
        //则直接应用其类型提供的spec_new_impl()函数

        t.spec_new_impl()

    }

    //此函数利用Vec构造CString

    pub unsafe fn from_vec_unchecked(v: Vec) -> Self {

        debug_assert!(memchr::memchr(0, &v).is_none());

        unsafe { Self::_from_vec_unchecked(v) }

    }

    //此函数利用已经通过安全检查的Vec构造CString

    unsafe fn _from_vec_unchecked(mut v: Vec) -> Self {

        //增加尾部的0值

        v.reserve_exact(1);

        v.push(0);

        Self { inner: v.into_boxed_slice() } //将堆内存从Vec结构转移到Box结构

    }

    //此函数利用C语言字符串构造CString, 此时C语言字符串应是通过into_raw()函数获取的

    pub unsafe fn from_raw(ptr: *mut c_char) -> CString {

        //ptr是通过CString::into_raw()函数获取的,使用该函数可以省略一次内存拷贝

        unsafe {

            let len = sys::strlen(ptr) + 1; //获取字符串长度

            //构造正确的切片引用

            let slice = slice::from_raw_parts_mut(ptr, len as usize);

            //构造CString

            CString { inner: Box::from_raw(slice as *mut [c_char] as *mut [u8]) }

        }

    }

    //此函数用于从CString变量获取C语言字符串变量

    pub fn into_raw(self) -> *mut c_char {

        Box::into_raw(self.into_inner()) as *mut c_char //CString已经包含了0值

    }

    //此函数用于将CString变量转换为String变量

    pub fn into_string(self) -> Result {

        String::from_utf8(self.into_bytes()).map_err(|e| IntoStringError {

            error: e.utf8_error(),

            //当出错时,恢复CString

            inner: unsafe { Self::_from_vec_unchecked(e.into_bytes()) },

        })

    }

    //此函数用于将CString变量转换为Vec变量

    pub fn into_bytes(self) -> Vec {

        //当消费了CString时,Box中的堆内存被转移到Vec

        let mut vec = self.into_inner().into_vec();

        let _nul = vec.pop(); //删掉尾部的0值

        debug_assert_eq!(_nul, Some(0u8));

        vec

    }

    pub fn into_bytes_with_nul(self) -> Vec {

        self.into_inner().into_vec() //对CString尾部的0值不做处理

    }

    //此函数用于将CString转换为[u8]切片引用

    pub fn as_bytes(&self) -> &[u8] {

        //删除尾部的0值

        unsafe { self.inner.get_unchecked(..self.inner.len() - 1) }

    }

    //此函数用于将CString转换为[u8]切片引用,不用删除尾部的0值

    pub fn as_bytes_with_nul(&self) -> &[u8] {  &self.inner  } //保留尾部的0值

    //此函数用于将CString转换为CStr的引用

    pub fn as_c_str(&self) -> &CStr {  &*self  }

    //此函数用于获取CString的inner成员

    fn into_inner(self) -> Box<[u8]> {

        //处理self的所有权,为后继read()函数做准备

        let this = mem::ManuallyDrop::new(self);

        unsafe { ptr::read(&this.inner) } //读取inner并获取所有权

    }

    //此函数利用Vec构造CString

    pub unsafe fn from_vec_with_nul_unchecked(v: Vec) -> Self {

        debug_assert!(memchr::memchr(0, &v).unwrap() + 1 == v.len());

        unsafe { Self::_from_vec_with_nul_unchecked(v) }

    }

    //无须再检查0值

    unsafe fn _from_vec_with_nul_unchecked(v: Vec) -> Self {

        Self { inner: v.into_boxed_slice() }

    }

    //此函数为从String转换为CString做支持

    pub fn from_vec_with_nul(v: Vec) -> Result {

        let nul_pos = memchr::memchr(0, &v); //确定0值的位置

        match nul_pos {

            Some(nul_pos) if nul_pos + 1 == v.len() => { //如果0值的位置正确,

                // 则构造CString

                Ok(unsafe { Self::_from_vec_with_nul_unchecked(v) })

            }

            //以下代码为出错处理

            Some(nul_pos) => Err(FromVecWithNulError {

                error_kind: FromBytesWithNulErrorKind::InteriorNul(nul_pos),

                bytes: v,

            }),

            None => Err(FromVecWithNulError {

                error_kind: FromBytesWithNulErrorKind::NotNulTerminated,

                bytes: v,

            }),

        }

    }

}

//实现Drop Trait

impl Drop for CString {

    fn drop(&mut self) {

        //此处代码说明CString内部的inner无法用转移所有权的方式解封装

        unsafe {

            //当消费了Box时,将字符串首字符设置为0,从而清空字符串

            *self.inner.get_unchecked_mut(0) = 0;

        }

    }

}

impl ops::Deref for CString {

    type Target = CStr;

    fn deref(&self) -> &CStr {

        unsafe { CStr::_from_bytes_with_nul_unchecked(self.as_bytes_with_nul()) }

    }

}

CStringCStr其他源代码的分析略。

4.OsString代码分析

传入SYSCALL的字符串参数采用的字符串类型很可能与C语言及Rust都不同,因此需要定义 OsStr及OsString作为系统字符串类型,实现对OS字符串类型的适配。显然,这个模块包含OS相关及OS无关两部分。

OsStr及OsString的OS无关适配层的源代码在标准库中的路径如下:

/library/src/std/src/ffi/os_str.rs    

OsStrOsStringOS相关适配层的源代码在标准库中的路径如下仅提供LinuxWindows中的路径):

/library/src/std/src/sys/unix/os_str.rs   

/library/src/std/src/sys/windows/os_str.rs  

OsStrOsString类型源代码的分析如下

// Linux的适配类型定义

pub struct Buf {  pub inner: Vec,  }

pub struct Slice {  pub inner: [u8],   }

//Windows的适配类型定义

pub struct Wtf8Buf {  bytes: Vec,  }

pub struct Wtf8 {  bytes: [u8],    }

pub struct Buf {  pub inner: Wtf8Buf,  }

pub struct Slice {   pub inner: Wtf8,   }

//OS无关的OsString及OsStr类型定义

pub struct OsString {   inner: Buf,   }

pub struct OsStr {  inner: Slice,  }

OsStringOsStrOS相关适配层实现的支持类型的基础上采用适配器设计模式来实现各种函数对这些函数的源代码分析如下

impl OsString {

    //此函数用于构造OsString类型变量

    pub fn new() -> OsString {

        //不同OS提供统一的Buf类型及函数

        OsString { inner: Buf::from_string(String::new()) }

    }

    //此函数用于消费self,返回String变量

    pub fn into_string(self) -> Result {

        self.inner.into_string().map_err(|buf| OsString { inner: buf })

    }

}

OsString及OsStr在Linux上的结构定义与Rust的String及Str一致,因此代码分析在此省略。

一文搞懂 Rust 与 C 语言交互的实现方式_第1张图片

以上内容节选自《深入RUST标准库:必备的RUST高级编程指南》
京东链接:
《深入Rust标准库:必备的Rust语言高级指南》(任成珺,等)【摘要 书评 试读】- 京东图书https://item.jd.com/14126571.html

你可能感兴趣的:(rust,c语言,交互)