Coder Social home page Coder Social logo

inline-c-rs's Introduction

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.

inline-c-rs's People

Contributors

hywan avatar jacobmischka avatar matze avatar mikayex avatar robert-steiner avatar syrusakbary avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

inline-c-rs's Issues

Object files left in source directory

Hi !

I'm on Windows and I've a project with the following layout (nothing fancy, juste a workspace):

.
├── crate1/
│   └── Cargo.toml
├── crate2/
│   ├── tests/
│   │   └── hello.rs
│   ├── src/
│   │   └── ...
│   └── Cargo.toml
└── Cargo.toml

The file hello.rs has a test with assert_c! (just the hello world example from the readme)

The problem

Compiled object files (.o/.obj) are created in crate2/inline-c-rs-XXXXXX.obj. A new one is created each time I run cargo test.

The solution ?

I saw you used the keep() method on the temporary input and output files.

Would it be the cause ? I may be wrong, I just looked quickly...

Doesn't allow declaring C macros

use inline_c::assert_c;

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

        #define sum(a,b) ((a) + (b))

        int main() {
            printf("%d", sum(1, 2));

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

This fails compiling the C code with

thread 'main' panicked at 'Unexpected failure.
code-1
stderr=```/tmp/inline-c-rs-9ZdBkB.c:5:9: error: expected declaration specifiers or ‘...’ before string constant
    5 | printf ("%d", sum (1, 2));
      |         ^~~~
/tmp/inline-c-rs-9ZdBkB.c:4:13: error: expected declaration specifiers or ‘...’ before ‘(’ token
    4 | #define sum (a , b )((a )+ (b ))int main (){
      |             ^
/tmp/inline-c-rs-9ZdBkB.c:5:15: note: in expansion of macro ‘sum’
    5 | printf ("%d", sum (1, 2));
      |               ^~~
/tmp/inline-c-rs-9ZdBkB.c:6:1: error: expected declaration specifiers or ‘...’ before ‘return’
    6 | return 0;
      | ^~~~~~
/tmp/inline-c-rs-9ZdBkB.c:8:1: error: expected declaration specifiers or ‘...’ before ‘}’ token
    8 | }
      | ^
/tmp/inline-c-rs-9ZdBkB.c:8:1: error: expected ‘;’, ‘,’ or ‘)’ before ‘}’ token

command="cc" "/tmp/inline-c-rs-9ZdBkB.c" "-O2" "-ffunction-sections" "-fdata-sections" "-fPIC" "-m64" "-Wall" "-Wextra" "-Werror" "-o" "/tmp/inline-c-rs-5lq9Fv"
code=1
stdout=``````
stderr=```/tmp/inline-c-rs-9ZdBkB.c:5:9: error: expected declaration specifiers or ‘...’ before string constant
5 | printf ("%d", sum (1, 2));
| ^~~~
/tmp/inline-c-rs-9ZdBkB.c:4:13: error: expected declaration specifiers or ‘...’ before ‘(’ token
4 | #define sum (a , b )((a )+ (b ))int main (){
| ^
/tmp/inline-c-rs-9ZdBkB.c:5:15: note: in expansion of macro ‘sum’
5 | printf ("%d", sum (1, 2));
| ^~~
/tmp/inline-c-rs-9ZdBkB.c:6:1: error: expected declaration specifiers or ‘...’ before ‘return’
6 | return 0;
| ^~~~~~
/tmp/inline-c-rs-9ZdBkB.c:8:1: error: expected declaration specifiers or ‘...’ before ‘}’ token
8 | }
| ^
/tmp/inline-c-rs-9ZdBkB.c:8:1: error: expected ‘;’, ‘,’ or ‘)’ before ‘}’ token

', /home/slomo/.cargo/registry/src/github.com-1ecc6299db9ec823/assert_cmd-1.0.2/src/assert.rs:158:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Doesn't allow declaring multi-line macros

This is different from #5 as here rustc already fails.

use inline_c::assert_c;

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

        #define sum(a,b) \
            ( \
              (a) \
              + \
              (b) \
            )

        int main() {
            printf("%d", sum(1, 2));

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

Fails compiling the Rust code with

error: unknown start of token: \
 --> src/main.rs:7:26
  |
7 |         #define sum(a,b) \
  |                          ^

error: unknown start of token: \
 --> src/main.rs:8:15
  |
8 |             ( \
  |               ^

error: unknown start of token: \
 --> src/main.rs:9:19
  |
9 |               (a) \
  |                   ^

error: unknown start of token: \
  --> src/main.rs:10:17
   |
10 |               + \
   |                 ^

error: unknown start of token: \
  --> src/main.rs:11:19
   |
11 |               (b) \
   |                   ^

error: aborting due to 5 previous errors

error: could not compile `tset`

To learn more, run the command again with --verbose.

Use of env vars like `CFLAGS` doesn't appear to support cross compilation

This is a re-post/elaboration of a comment I made on reddit

Looking at the use of CFLAGS, etc, env variables, it would be a good idea to mirror the pattern used in the cc crate. From the cc readme:

Each of these variables can also be supplied with certain prefixes and suffixes, in the following prioritized order:

  1. <var>_<target> - for example, CC_x86_64-unknown-linux-gnu
  2. <var>_<target_with_underscores> - for example, CC_x86_64_unknown_linux_gnu
  3. <build-kind>_<var> - for example, HOST_CC or TARGET_CFLAGS
  4. <var> - a plain CC, AR as above.

We used this specific mechanism (of resolving most specific to least specific without combining/appending) to best support the cross compilation of C code.

The relevant code in inline-c appears here:

inline-c-rs/src/run.rs

Lines 149 to 162 in 7bd0f5e

let get_env_flags = |env_name: &str| -> Vec<String> {
variables
.get(env_name)
.map(|e| e.to_string())
.ok_or_else(|| env::var(env_name))
.unwrap_or(String::new())
.split_ascii_whitespace()
.map(|slice| slice.to_string())
.collect()
};
command.args(get_env_flags("CFLAGS"));
command.args(get_env_flags("CPPFLAGS"));
command.args(get_env_flags("CXXFLAGS"));

Given the desire to integrate CFLAGS/etc that are provided by the program rather than the environment, I'm not sure exactly what should be done here. The CFLAGS/etc from the environment are usually supplied in order for compilation to be functional. Given this, it seems unwise to have program supplied values replace those from the environment. Appending the program supplied values to the environment supplied values might be the best choice.

Additionally, it would be good to examine the splitting methodology used for these variables. In some cases, CFLAGS/etc may contain spaces. This is more common on windows when one might need to provide a path to some directory within the LDFLAGS or CFLAGS (or for plain CC).

On a related note: the handling of LDFLAGS using -Wl probably isn't compatible with how other users interpret LDFLAGS (which are normally passed to the link step without extra munging).

LDFLAGS mapping breaks on msvc

As seen on here/ci:

stderr=```cl : Command line error D8021 : invalid numeric argument '/Wl,-LD:\a\cargo-c\cargo-c\example-project\target\.\release'

`assert_c!` should be `unsafe_assert_c!`

As it is, this would be an unsound library as it allows you to cause undefined behaviour with safe code. I believe simply inserting unsafe in the macro name would fix this: I recall seeing this in some threads about some macros doing unsafe things, though I cannot find anything.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.