inline-c
is a small crate that allows a user to write C (including C++) code inside Rust. Both environments are strictly sandboxed: it is non-obvious for a value to cross the boundary. The C code is transformed into a string which is written in a temporary file. This file is then compiled into an object file, that is finally executed. It is possible to run assertions about the execution of the C program.
The primary goal of inline-c
is to ease the testing of a C API of a Rust program (generated with cbindgen
for example). Note that it's not tied to a Rust program exclusively, it's just its initial reason to live.
Add the following lines to your Cargo.toml
file:
[dev-dependencies]
inline-c = "0.1"
The assert_c
and assert_cxx
macros live in the inline-c-macro
crate, but are re-exported in this crate for the sake of simplicity.
Being able to write C code directly in Rust offers nice opportunities, like having C examples inside the Rust documentation that are executable and thus tested (with cargo test --doc
). Let's dig into some examples.
The following example is super basic: C prints Hello, World!
on the standard output, and Rust asserts that.
use inline_c::assert_c;
fn test_stdout() {
(assert_c! {
#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}
})
.success()
.stdout("Hello, World!");
}
Or with a C++ program:
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!");
}
The assert_c
and assert_cxx
macros return a Result<Assert, Box<dyn Error>>
. See Assert
to learn more about the possible assertions.
The following example tests the returned value:
use inline_c::assert_c;
fn test_result() {
(assert_c! {
int main() {
int x = 1;
int y = 2;
return x + y;
}
})
.failure()
.code(3);
}
It is possible to define environment variables for the execution of the given C program. The syntax is using the special #inline_c_rs
C directive with the following syntax:
#inline_c_rs <variable_name>: "<variable_value>"
Please note the double quotes around the 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`");
}
Using the #inline_c_rs
C directive can be repetitive if one needs to define the same environment variable again and again. That's why meta environment variables exist. They have the following syntax:
INLINE_C_RS_<variable_name>=<variable_value>
It is usually best to define them in a build.rs
script for example. Let's see it in action with a tiny example:
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");
}
CFLAGS
, CPPFLAGS
, CXXFLAGS
and LDFLAGS
Some classical Makefile
variables like CFLAGS
, CPPFLAGS
, CXXFLAGS
and LDFLAGS
are understood by inline-c
and consequently have a special treatment. Their values are added to the appropriate compilers when the C code is compiled and linked into an object file.
Pro tip: Let's say we have a Rust crate named foo
, and it exports a C API. It is possible to define CFLAGS
and LDFLAGS
as follow to correctly compile and link all the C codes to the Rust libfoo
shared object by writing this in a build.rs
script (it is assumed that libfoo
lands in the target/<profile>/
directory, and that foo.h
lands in the root directory):
use std::{env, ffi::OsStr};
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()
}
);
}
Et voilà ! Now run cargo build --release
(to generate the shared objects) and then cargo test --release
to see it in action.
inline-c
inside Rust documentationSince it is now possible to write C code inside Rust, it is consequently possible to write C examples, that are:
cargo doc
, andcargo test --doc
.Yes. Testing C code with cargo test --doc
. How fun is that? No trick needed. One can write:
/// 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() {}
which will compile down into something like this:
int main() {
printf("Hello, World!");
return 0;
}
Notice that this example above is actually Rust code, with C code inside. Only the C code is printed, due to the #
hack of rustdoc
, but this example is a valid Rust example, and is fully tested!
There is one minor caveat though: the highlighting. The Rust set of rules are applied, rather than the C ruleset. See this issue on rustdoc
to follow the fix.
C macros with the #define
directive is supported only with Rust nightly. One can write:
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();
}
Note that multi-lines macros don't work! That's because the \
symbol is consumed by the Rust lexer. The best workaround is to define the macro in another .h
file, and to include it with the #include
directive.
inline-c
for you when using cargo ctest
!BSD-3-Clause
, see LICENSE.md
.
Version | Tag | Published |
---|---|---|
0.1.4 | 2yrs ago | |
0.1.3 | 2yrs ago | |
0.1.2 | 2yrs ago | |
0.1.1 | 2yrs ago |