Crate inline_c[−][src]
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. Note that it's not tied to a Rust program
exclusively, it's just its initial reason to live.
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.
Basic usage
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> int main() { std::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); }
Environment variables
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`"); }
Meta environment variables
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.
Using inline-c
inside Rust documentation
Since it is now possible to write C code inside Rust, it is consequently possible to write C examples, that are:
- Part of the Rust documentation with
cargo doc
, and - Tested with all the other Rust examples with
cargo 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:
#include <stdio.h> 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
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.
Modules
predicates | Re-export the prelude of the |
Macros
assert_c | |
assert_cxx |
Structs
Assert |
|