"Wouldn't it be neat if you could write C++ inline in Rust?"

In June of 2015, I had an idea. At the time, I was obsessed with compilers, and what was possible to do at compile time. For one of the languages which I was working on, I got excited by the idea that I could have flawless C++ interop by embedding all of clang inside of the compiler, and have a special c++ {} form which would allow you to directly write C++ code inside the program, and give that code access to the stack variables currently in scope.

That language never came into fruition, but at the time I was also interested in another language, which I was going to use to implement my language, Rust. Rust was very interesting to me, because in 2014 it had been one of the first times I had ever written "system-level" code. I loved the way it seemed to make everything possible with apparently no overhead. However, I had run into some problems. I wanted to use LLVM to implement the compiler back-end for the language I was working on, but the best LLVM bindings were written in C++, and using them from Rust was a tedious experience, to say the least. No good LLVM bindings were available yet for Rust, so I would pretty much have to write them myself.

I, naturally, started wishing that I had the c++ {} block feature in Rust, to help me write my new programming language. I really enjoy abusing meta programming features in languages to let me do things which their creators never intended, so I started concocting ideas as to how I could implement this using Rust's unstable plugin infrastructure.

Thus, rust-cpp was born.

You can still find the code for this original version of rust-cpp, although you'll have trouble getting it to build, archived under the legacy_rustc_plugin branch.

This initial version of rust-cpp was built with 2 parts, and remains possibly the most powerful version of rust-cpp to this day. Firstly, it contained a procedural macro, cpp!, which would perform the Rust codegen, and store the information parsed in some global state, and then a lint pass, which would discover the type information for the callsites, and actually generate and compile the C++ code.

An example use of this API might look like:

let a: i32 = 10;
let b: i32 = 20;

let cpp_result = unsafe {
    cpp!((a, b) -> i32 {
        int32_t c = a * 10;
        int32_t d = b * 20;

        return c + d;
    })
};

This invocation would "capture" the local variables a and b in the cpp! closure, exposing those variable names to the C++ code. the closure itself then would return a i32. The C++ code would be contained with a function invocation, such that the interface looks correct.

The cpp! macro expansion would produce some code which would look something like the following:

{
    #[link(name = "rust_cpp_tmp", kind = "static")]
    extern "C" {
        fn rust_cpp_936DA01F9ABD4d9d80C702AF85C822A8(a: *const u8, b: *const u8) -> i32;
    }
    rust_cpp_936DA01F9ABD4d9d80C702AF85C822A8(&a as *const _ as *const u8, &b as *const _ as *const u8)
}

It would then also record the information parsed from the declaration (the names of the arguments, the body text extracted from the original span as a string, etc.) in the global storage.

Then, the lint pass would run. It would walk the typechecked AST, looking at every function call. If the function call begain with the name rust_cpp_, it would be considered as a rust-cpp call. The matching function would be looked up from global storage.

We would then look at the pre-casting types of a and b, and try to guess the C++ type from them. If we failed, we would default to passing an opaque type to C++.

Once the lint pass had seen every function call which was generated earlier by the cpp! procedural macro, it would write out a rust_cpp_tmp.cpp file, which would then be shelled out to the native c++ compiler. The above function would have been generated similar to the following:

/******************************
 * Code Generated by Rust-C++ *
 ******************************/

/* cstdint includes sane type definitions for integer types */
#include <cstdint>

/* the rs:: namespace contains rust-defined types */
namespace rs {
    /* A slice from rust code */
    /* Can be used to interact with, pass around, and return Rust slices */
    template<class T>
    struct Slice {
        const T*  data;
        uintptr_t len;
    };

    /* A string slice is simply a slice of utf-8 encoded characters */
    typedef Slice<uint8_t> StrSlice;

    /* A trait object is composed of a data pointer and a vtable */
    struct TraitObject {
        void* data;
        void* vtable;
    };

    /* A dummy struct which is generated when incompatible types are closed-over */
    struct __Dummy;


    /* Typedefs for integral and floating point types */
    typedef uint8_t u8;
    typedef uint16_t u16;
    typedef uint32_t u32;
    typedef uint64_t u64;
    typedef uint64_t usize;

    typedef int8_t i8;
    typedef int16_t i16;
    typedef int32_t i32;
    typedef int64_t i64;
    typedef int64_t isize;

    typedef float f32;
    static_assert(sizeof(f32) == 4, "C++ `float` isn't 32 bits wide");

    typedef double f64;
    static_assert(sizeof(f64) == 8, "C++ `double` isn't 64 bits wide");

    /* We use this bool type to ensure that our bools are 1 byte wide */
    typedef i8 bool_;
}

/* User-generated function declarations */
extern "C" {

::rs::i32 rust_cpp_936DA01F9ABD4d9d80C702AF85C822A8(const ::rs::i32& a, const ::rs::i32& b) {
    int32_t c = a * 10;
    int32_t d = b * 20;

    return c + d;
}

}

The code would then use an ugly hack to insert the compiled static library into the SearchPaths object, causing it to be linked in when the compiler performs the final link step.

This plugin was pretty cool. It could allow you to embed arbitrary C++ code into your Rust code, was fairly easy to add, and inferred a lot of types for you to boot! Unfortunately, it required unstable Rust, which meant that people definitely couldn't use it, and also meant that I had to push a new version very frequently to keep up with bustage.

Eventually, I decided to rewrite rust-cpp, and shed some of the cool features, in persuit of it working on stable Rust, but that's a story for part 2.

Go Top