inline-c

Crates.ioinline-c
lib.rsinline-c
version0.1.7
sourcesrc
created_at2020-11-05 13:45:59.538193
updated_at2022-08-16 06:11:39.110805
descriptionWrite and execute C code inside Rust
homepagehttps://github.com/Hywan/inline-c-rs
repositoryhttps://github.com/Hywan/inline-c-rs
max_upload_size
id308894
size132,426
Ivan Enderlin (Hywan)

documentation

README

Lilac-breated Roller, by David Clode
inline-c

crates.io documentation

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.

Install

Add the following lines to your Cargo.toml file:

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

Documentation

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>

        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);
}

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, 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()
        }
    );
}

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:

  1. Part of the Rust documentation with cargo doc, and
  2. 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:

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.

Who is using it?

  • Wasmer, the leading WebAssembly runtime,
  • Cargo C, to build and install C-compatible libraries; it configures inline-c for you when using cargo ctest!
  • Biscuit, an authorization token microservices architectures.

License

BSD-3-Clause, see LICENSE.md.

Commit count: 95

cargo fmt