#cpp #c #env-var #test

开发工具 inline-c

在Rust中编写和执行C代码

8次发布

0.1.7 2022年8月16日
0.1.6 2021年5月17日
0.1.5 2021年2月12日
0.1.4 2020年11月30日

#287 in FFI

Download history 1618/week @ 2024-03-14 1785/week @ 2024-03-21 1497/week @ 2024-03-28 2463/week @ 2024-04-04 4888/week @ 2024-04-11 2087/week @ 2024-04-18 8429/week @ 2024-04-25 1467/week @ 2024-05-02 4028/week @ 2024-05-09 5974/week @ 2024-05-16 7948/week @ 2024-05-23 6819/week @ 2024-05-30 3254/week @ 2024-06-06 1740/week @ 2024-06-13 1793/week @ 2024-06-20 891/week @ 2024-06-27

8,112 每月下载量
用于 10 crates

BSD-3-Clause

110KB
345 代码行

Lilac-breated Roller, by David Clode
inline-c

crates.io documentation

inline-c 是一个小型的Crate,允许用户在Rust中编写C(包括C++)代码。这两个环境都是严格沙箱化的:值跨越边界的可能性不明显。C代码被转换成一个字符串,然后写入临时文件。然后该文件被编译成目标文件,最终执行。可以运行关于C程序执行的断言。

inline-c 的主要目标是简化Rust程序C API的测试(例如,使用 cbindgen 生成)。请注意,它不仅仅与Rust程序绑定,它只是其存在的初始原因。

安装

将以下行添加到您的 Cargo.toml 文件中

[dev-dependencies]
inline-c = "0.1"

文档

assert_cassert_cxx 宏位于 inline-c-macro Crate中,但为了简单起见,在此Crate中重新导出。

能够在Rust中直接编写C代码提供了很好的机会,例如,在Rust文档中包含可执行的C示例,从而可以进行测试(使用 cargo test --doc)。让我们看看一些例子。

基本用法

以下示例非常基础:C在标准输出上打印 Hello, World!,Rust断言这一点。

use inline_c::assert_c;

fn test_stdout() {
    (assert_c! {
        #include <stdio.h>

        int main() {
            printf("Hello, World!");

            return 0;
        }
    })
    .success()
    .stdout("Hello, World!");
}

或使用C++程序

use inline_c::assert_cxx;

fn test_cxx() {
    (assert_cxx! {
        #include <iostream>

        using namespace std;

        int main() {
            cout << "Hello, World!";

            return 0;
        }
    })
    .success()
    .stdout("Hello, World!");
}

assert_cassert_cxx 宏返回一个 Result<Assert, Box<dyn Error>>。有关可能的断言的更多信息,请参阅 Assert

以下示例测试返回值

use inline_c::assert_c;

fn test_result() {
    (assert_c! {
        int main() {
            int x = 1;
            int y = 2;

            return x + y;
        }
    })
    .failure()
    .code(3);
}

环境变量

可以为给定C程序的执行定义环境变量。语法是使用特殊的#inline_c_rs C指令,其语法如下

#inline_c_rs <variable_name>: "<variable_value>"

请注意变量值周围的双引号。

use inline_c::assert_c;

fn test_environment_variable() {
    (assert_c! {
        #inline_c_rs FOO: "bar baz qux"

        #include <stdio.h>
        #include <stdlib.h>

        int main() {
            const char* foo = getenv("FOO");

            if (NULL == foo) {
                return 1;
            }

            printf("FOO is set to `%s`", foo);

            return 0;
        }
    })
    .success()
    .stdout("FOO is set to `bar baz qux`");
}

元环境变量

如果需要重复定义相同的环境变量,使用#inline_c_rs C指令可能会变得繁琐。这就是为什么存在元环境变量的原因。它们具有以下语法

INLINE_C_RS_<variable_name>=<variable_value>

通常最好在一个build.rs脚本中定义它们,例如。让我们用一个微小的例子来看看它是如何工作的

use inline_c::assert_c;
use std::env::{set_var, remove_var};

fn test_meta_environment_variable() {
    set_var("INLINE_C_RS_FOO", "bar baz qux");

    (assert_c! {
        #include <stdio.h>
        #include <stdlib.h>

        int main() {
            const char* foo = getenv("FOO");

            if (NULL == foo) {
                return 1;
            }

            printf("FOO is set to `%s`", foo);

            return 0;
        }
    })
    .success()
    .stdout("FOO is set to `bar baz qux`");

    remove_var("INLINE_C_RS_FOO");
}

CFLAGSCPPFLAGSCXXFLAGSLDFLAGS

一些经典的Makefile变量如CFLAGSCPPFLAGSCXXFLAGSLDFLAGSinline-c所理解,因此有特殊处理。它们的值在将C代码编译并链接到目标文件时添加到相应的编译器中。

提示:假设我们有一个名为foo的Rust包,并且它导出了一个C API。我们可以定义CFLAGSLDFLAGS如下,以通过在build.rs脚本中写入以下内容来正确编译和链接所有C代码到Rust的libfoo共享对象(假设libfoo位于target/<profile>/目录中,并且foo.h位于根目录中)

use std::{env, path::PathBuf};

fn main() {
    let include_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    let mut shared_object_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    shared_object_dir.push("target");
    shared_object_dir.push(env::var("PROFILE").unwrap());
    let shared_object_dir = shared_object_dir.as_path().to_string_lossy();

    // The following options mean:
    //
    // * `-I`, add `include_dir` to include search path,
    // * `-L`, add `shared_object_dir` to library search path,
    // * `-D_DEBUG`, enable debug mode to enable `assert.h`.
    println!(
        "cargo:rustc-env=INLINE_C_RS_CFLAGS=-I{I} -L{L} -D_DEBUG",
        I = include_dir,
        L = shared_object_dir.clone(),
    );

    // Here, we pass the fullpath to the shared object with
    // `LDFLAGS`.
    println!(
        "cargo:rustc-env=INLINE_C_RS_LDFLAGS={shared_object_dir}/{lib}",
        shared_object_dir = shared_object_dir,
        lib = if cfg!(target_os = "windows") {
            "foo.dll".to_string()
        } else if cfg!(target_os = "macos") {
            "libfoo.dylib".to_string()
        } else {
            "libfoo.so".to_string()
        }
    );
}

太棒了!现在运行cargo build --release(生成共享对象)然后运行cargo test --release来查看它的作用。

在Rust文档中使用inline-c

由于现在可以在Rust中编写C代码,因此可以相应地编写C示例,这些示例是

  1. 作为Rust文档的一部分,使用cargo doc,并且
  2. 与其他Rust示例一起使用cargo test --doc进行测试。

是的。使用cargo test --doc测试C代码。这有多有趣?不需要任何技巧。可以写

/// Blah blah blah.
///
/// # Example
///
/// ```rust
/// # use inline_c::assert_c;
/// #
/// # fn main() {
/// #     (assert_c! {
/// #include <stdio.h>
///
/// int main() {
///     printf("Hello, World!");
///
///     return 0;
/// }
/// #    })
/// #    .success()
/// #    .stdout("Hello, World!");
/// # }
/// ```
pub extern "C" fn some_function() {}

这将编译成类似下面的内容

int main() {
    printf("Hello, World!");

    return 0;
}

请注意,上面的示例实际上是Rust代码,其中包含C代码。由于rustdoc#技巧,只打印了C代码,但这是一个有效的Rust示例,并且已完全测试!

但是有一个小的限制:高亮显示。应用的是Rust的规则集,而不是C的规则集。请参阅此关于rustdoc的问题以跟踪修复

C 宏

C 宏通过 #define 指令仅支持 Rust nightly 版本。可以这样写:

use inline_c::assert_c;

fn test_c_macro() {
    (assert_c! {
        #define sum(a, b) ((a) + (b))

        int main() {
            return !(sum(1, 2) == 3);
        }
    })
    .success();
}

注意,多行宏不起作用!这是因为 Rust 词法分析器消耗了 \ 符号。最好的解决方案是将宏定义在另一个 .h 文件中,并使用 #include 指令包含它。

谁在使用它?

  • Wasmer,领先的 WebAssembly 运行时,
  • Cargo C,用于构建和安装与 C 兼容的库;当使用 cargo ctest 时,它为您配置 inline-c
  • Biscuit,一个授权令牌微服务架构。

许可证

BSD-3-Clause,见 LICENSE.md

依赖项

~5–15MB
~196K SLoC