更复杂的数据类型

对于交互接口中的简单类型,我们直接使用标准库中定义好的std::os::raw 里面的 类型就够了。而更复杂的类型就需要我们手动封装了。比如结构体就需要用#[repr©]修饰,以保证这个结构体在Rust和 C 双方的内存布局是一致的。

如果我们需要做的是跟常见的操作系统交互,许多常用的数据结构都已经有人封装好了, 可以在crates.io 找libc 库直接使用。接下来我们用一个示例演示一下在接口中包含结构体该怎 样做。这个示例同时也使用了cargo来管理Rust项目,且使用动态链接库的方式执行。

Rust 项 目Cargo.toml 如 下 所 示 :

[package] name = "log"version = "0.1.0"

authors = ["F001"][dependencies] libc = "0.2" [lib]

src / lib.rs文件内容如下所示:# ! [crate_type = "cdylib"] extern crate libc;
use libc: :{
    c_int,
    c_char
};
use std: :ffi: :CStr;
//this struct is used as an public interface #[repr(C)]
# [no_mangle] pub struct RustLogMessage {
    id: c_int,
    msg: *const c_char
}# [no_mangle] pub extern "C"fn rust_log(msg: RustLogMessage) {
    let s = unsafe {
        CStr: :from_ptr(msg.msg)
    };
    println ! ("id:{}message:{:?}", msg.id, s);
}

使 用cargo build 就可以编译出对应的动态库。C 语言的调用代码如下所示:

#include < stdio.h > #include < stdlib.h > #include < dlfcn.h > struct RustLogMessage {
    int id;
    char * msg;
};
int main() {
    void * rust_log_lib;
    void( * rust_log_fn)(struct RustLogMessage msg);
    rust_log_lib = dlopen("./rust_log/target/debug/librust_log.so", RTLD_LAZY);
    if (rust_log_lib != NULL) {
        rust_log_fn = dlsym(rust_log_lib, "rust_log");
    } else {
        printf("load so library failed.\n");
        return 1;
    }
    for (int i = 0; i < 10; i++) {
        struct RustLogMessage msg = {
            id: i,
            msg: "string          in          c\n",
        };
        rust_log_fn(msg);
    }
    if (rust_log_lib != NULL) dlclose(rust_log_lib);
    return EXIT_sUCCESS;

编 译 命 令 为gcc main.c -ldl。执行程序之前,要保证动态链接库和可执行程序之间 的相对路径关系是正确的,然后执行。可见,参数正确地在Rust 和 C 之 间 传 递 了 。


文档

Rust 也支持使用注释来编写规范的文档。可以使用rustdoc 工具把源码中的文档提取出来,生成易读的HTML 等格式。在cargo 里面可以用cargo doc 命令生成文档。

普通的注释有两种,一种是用//开头的,是行注释,一种是/* *1,是块注释。这 些注释不会被视为文档的一部分。特殊的文档是//1、//!、/**… 1、/!… */,它们会被视为文档。

跟 attribute 的规则类似:用///开头的文档被视为是给它后面的那个元素做的说明; //!开头的文档被视为是给包含这块文档的元素做的说明。 /**… * / 和 / * ! … * / 也 是 类似的。示例如下:

mod foo {
    //!这块文档是给‘foo`    模块做的说明
    ///这块文档是给函数‘f` 做的说明
    fn f() {
        //这块注释不是文档的一部分
    }
}

文档内部支持markdown 格式。可以使用#作为 一 级标题。比如标准库中常用的几种 标题 :

///#Panics
///#Errors
/ 1 / #Safety

///#Examples
文档中的代码部分要用符号把它们括起来。代码块应该用括起来。比如:
///
///let mut vec =Vec::with_capacity(10);
///
/ 11 //The  vector  contains  no  items,even  though  it  has  capacity  for  more
/ 1 / assert_eq ! (vec.len(), 0);
///

Rust 文档里面的代码块,在使用cargo test 命令时,也是会被当做测试用例执行的。这个设计可以在很多时候检查出文档和代码不对应的情况。

如果文档太长,也可以写在单独的markdown 文件中。如果在单独的文件中写文档,就 不需要再用///或者//!开头了,直接写内容就可以。然后再用一个attribute来指定给对 应的元素:

# ! [feature(external_doc)]# [doc(include = "external-doc.md")] pub struct MyAwesomeType;

测 试

Rust 内置了一套单元测试框架。单元测试是一种目前业界广泛使用的,可以显著提升代 码可靠性的工程管理手段。Rust 里面的单元测试代码可以直接和业务代码写在一个文件中, 非常有利于管理,方便更新。执行单元测试也非常简单,一条cargo test命令即可。
一般情况下,如果我们新建一个library项目,cargo工具会帮我们在src/lib.rs 中自 动生成如下代码:

# [cfg(test)] mod tests {# [test] fn it_works() {}
}

这就是最基本的单元测试框架。下面详细介绍一下这里面的各个要素。

首先,Rust 里面有一个特殊的attribute, 叫作#[cfg] 。 它主要是用于实现各种条件编 译。比如#[cfg(test)] 意思是,这部分代码只在test 这个开关打开的时候才会被编译。

它还有更高级的用法,比如

# [cfg(any(unix, windows))]# [cfg(all(unix, target_pointer_width = "32"))]# [cfg(not(foo))]

# [cfg(any(not(unix), all(target_os = "macos", target_arch = "powerpc")))]我们还可以自定义一些功能开关。比如在Cargo.toml中加入这样的代码: [features]#默认开启的功能开关
default = []#定义一个新的功能开关,以及它所依赖的其他功能#我们定义的这个功能不依赖其他功能,默认没有开启
my_feature_name = []之后就可以在代码中使用这个功能开关,某部分代码可以根据这个开关的状态决定编译还是不编译:
# [cfg(feature = "my_feature_name")] mod sub_module_name {}

这个开关究竟是开还是关,可以通过编译选项传递进去:

cargo build --features "my_feature_name"

当我们使用cargo test 命令的时候,被#[cfg(test)] 标记的代码就会被编译执 行 ; 否 则 直 接 被 忽 略 。我们还是用一个示例来说明。我们现在准备实现一个辗转相除法求最大公约数的功能。 新建一个名叫gcd 的项目:

cargo new --lib  gcd

辗转相除法的细节就不展开了。实现代码如下所示:

pub fn gcd(a: u64, b: u64) - >u64 {
    let(m(a(ut), b) 1, mut g) =
    if a < b {} else { (b, a)
    };
    while 1 != 0 {
        let m = g 81;
        g = 1;
        1 = m;
    }
    return g;
}

接下来添加一个最基本的测试 :

			# [cfg(test)] mod tests {# [test] fn it_works() {

        assert_eq ! (gcd(2, 3), 1);
    }
}

使用cargo test 命令执行这个测试。这一次发生了编译错误,编译器找不到gcd 这个函 数。这是因为我们把测试用例写在了一个单独的模块中,在子模块中并不能直接访问父模块 中的内容。在mod 内部加一句use gcd; 或者use super:😗;可以解决这个问题。

Compiling       gcd       v0.1.0(file:///projects/gcd)
Finished    dev     [unoptimized    +debuginfo]target(s)in    2.33     secs Running   target/debug/deps/gcd-1658b34blde16a01
running   1  test
test    tests::it_works   ..ok
test     result:ok.1     passed;0      failed;0     ignored;0     measured;0      filtered     out Doc-tests gcd
running   0   tests
test     result:ok.0     passed;0      failed;0     ignored;0     measured;0      filtered     out

打印出来的结果非常清晰易读。前面一部分是执行测试模块中的测试用例的结果,后面 一部分是执行文档中的测试用例的结果。1 passed 代表通过了1个测试。0 failed 代表 失败了0个测试。0 ignored 代表忽略了0个测试。
用户可以用#[ignore] 标记测试用例,暂时忽略这个测试。比如:

# [test]# [ignore] fn it_works() {
    assert_eq ! (gcd(2, 3), 1);
}
// measured代表跑了0个benchmark性能测试。我们可以用# [bench]添加性能测试用例:# [cfg(test)] mod tests {
    use super: :*;
    use self: :test: :Bencher;# [bench] fn big_num(b: &mut Bencher) {
        b.iter( | l gcd(12345, 67890))
    }
}

这个功能目前还没有稳定,需要用户在当前crate中开启feature gate:

#![feature(test)]
extern crate test;

然后使用cargo bench就可以执行这个性能测试。这时就可以看到测试结果中有1 measured的结果。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐