The Rust side

There are new things in the Cargo.toml file that we need to discuss:

[package]
name = "embed_quantiles"
version = "0.1.0"
authors = ["Brian L. Troutwine <[email protected]>"]

[dependencies]
quantiles = "0.7"

[lib]
name = "embed_quantiles"
crate-type = ["staticlib"]

Note specifically that we're building the embed_quantiles library with crate-type = ["staticlib"]. What does that mean? The Rust compiler is capable of making many kinds of linkages, and usually they're just implicit. For instance, a binary is crate-type = ["bin"]. There's a longish list of different link types, which I've included in the Further reading section. The interested reader is encouraged to look through them. What we'd like to produce here is a statically linked library, otherwise every C programmer that tries to make use of embed_quantiles is going to need Rust's shared libraries installed on their system. Not… not great. A staticlib crate will archive all the bits of the Rust standard library needed by the Rust code. The archive can then be linked to C as normal.

Okay, now, if we're going to produce a static archive that C can call, we've got to export Rust functions with a C ABI. Put another way, we've got to put a C skin on top of Rust. C++ programmers will be familiar with this tactic. The sole Rust file in this project is src/lib.rs and its preamble is what you might expect:

extern crate quantiles;

use quantiles::ckms::CKMS;

We've pulled in the quantiles library and have imported CKMS. No big deal. Check this out, though:

#[no_mangle]
pub extern "C" fn alloc_ckms(error: f64) -> *mut CKMS<f32> {
    let ckms = Box::new(CKMS::new(error));
    Box::into_raw(ckms)
}

Hey! There's a bunch of new things. First, #[no_mangle]? A static library has to export a symbol for the linker to, well, link. These symbols are functions, static variables, and so forth. Now, usually Rust is free to fiddle with the names that go into a symbol to include information, such as module location or, really, whatever else the compiler wants to do. The exact semantics of mangling are undefined, as of this writing. If we're going to be calling a function from C, we have to have the exact symbol name to refer to. no_mangle turns off mangling, leaving us with our name as written. It does mean we have to be careful not to cause symbol collisions. Similar to importing functions, the extern C here means that this function should be written out to obey the C ABI. Technically, we could also have written this as extern fn, leaving the C off as the C ABI is the implicit default.

alloc_ckms allocates a new CKMS, returning a mutable pointer to it. Interop with C requires raw pointers, which, you know, makes sense. We do have to be very conscious of memory ownership when embedding Rust—does Rust own the memory, implying we need to provide a free function? Or, does the other language own the memory? More often than not, it's easier to keep ownership with Rust, because to free memory, the compiler will need to know the type's size in memory. By passing a pointer out, as we're doing here, we've kept C in the dark about the size of CKMS. All the C side of this project knows is that it has an opaque struct to deal with. This is a common tactic in C libraries, for good reason. Here's freeing a CKMS:

#[no_mangle]
pub extern "C" fn free_ckms(ckms: *mut CKMS<f32>) -> () {
    unsafe {
        let ckms = Box::from_raw(ckms);
        drop(ckms);
    }
}

Notice that in alloc_ckms, we're boxing the CKMS—forcing it to the heap—and in free_ckms we're building a boxed CKMS from its pointer. We discussed boxing and freeing memory in the context of raw pointers extensively in Chapter 3, The Rust Memory Model – Ownership, References and Manipulation. Inserting a value into the CKMS is straightforward enough:

#[no_mangle]
pub extern "C" fn ckms_insert(ckms: &mut CKMS<f32>, value: f32) -> () {
    ckms.insert(value)
}

Querying requires a little explanation:

#[no_mangle]
pub extern "C" fn query(ckms: &mut CKMS<f32>, q: f64, 
quant: *mut f32)
-> i8
{ unsafe { if let Some((_, res)) = ckms.query(q) { *quant = res; 0 } else { -1 } } }

Signaling error conditions in a C API is tricky. In Rust, we return some kind of compound type, such as an Option. There's no such thing in C without building an error signaling struct for your API. In addition to the error-struct approach it's common to either write well-known nonsense into the pointer where the answer will be written or return a negative value. C expects to have its answer written into a 32-bit float being pointed to by quant, and there's no easy way to write nonsense into a numeric value. So, query returns an i8; zero on success, negative on a failure. A more elaborate API would differentiate failures by returning different negative values.

That's it! When you run cargo build --release, a static library obeying the C ABI will get kicked out into target/release. We're ready to link it into a C program.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.72.176