laitimes

Secure interoperability between Rust and C++ with CXX

author:Not bald programmer
Secure interoperability between Rust and C++ with CXX

CXX is a library that provides a secure invocation mechanism between Rust and C++ that avoids all the problems that can arise when using bindgen or cbindgen to generate insecure C-style bindings. This article will show you how to configure and use the CXX library, discuss its key features, and provide some sample code.

Introduction to CXX

The CXX library provides a mechanism to call C++ code from Rust and Rust code from C++. We do this by defining the FFI boundaries on both sides in a Rust module, and then CXX does a static analysis to ensure the correctness of the type and function signatures. CXX generates the corresponding extern "C" signature to ensure that it is verified correctly during the build process. The resulting FFI bridge has little overhead at runtime and does not involve replication, serialization, memory allocation, or runtime checks.

Installation and configuration

First, we need to add the CXX library as a dependency in the Cargo.toml file of the Rust project:

[dependencies]
cxx = "1.0"

[build-dependencies]
cxx-build = "1.0"           

Then, we need to create a build script build.rs that runs CXX's C++ code generator and compile the generated C++ code:

// build.rs

fn main() {
    cxx_build::bridge("src/main.rs")  // 返回一个 cc::Build 实例
        .file("src/demo.cc")
        .std("c++11")
        .compile("cxxbridge-demo");

    println!("cargo:rerun-if-changed=src/main.rs");
    println!("cargo:rerun-if-changed=src/demo.cc");
    println!("cargo:rerun-if-changed=include/demo.h");
}           

Sample code

Let's use a concrete example to show how CXX can be used to interoperate Rust and C++. Let's say we have an existing C++ client for a large file storage service, and we want to leverage this client in our Rust application.

1. Define FFI boundaries

In Rust code, we define two static external blocks, one for Rust and one for C++.

// src/main.rs

#[cxx::bridge]
mod ffi {
    #[derive(Debug)]
    struct BlobMetadata {
        size: usize,
        tags: Vec<String>,
    }

    extern "Rust" {
        type MultiBuf;

        fn next_chunk(buf: &mut MultiBuf) -> &[u8];
    }

    unsafe extern "C++" {
        include!("demo/include/blobstore.h");

        type BlobstoreClient;

        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
        fn put(&self, parts: &mut MultiBuf) -> u64;
        fn tag(&self, blobid: u64, tag: &str);
        fn metadata(&self, blobid: u64) -> BlobMetadata;
    }
}           

2. Implement methods in Rust

We implement the necessary methods for the Rust part. For example, MultiBuf structs and next_chunk functions:

// src/main.rs

pub struct MultiBuf {
    chunks: Vec<Vec<u8>>,
    current: usize,
}

impl MultiBuf {
    pub fn new(chunks: Vec<Vec<u8>>) -> Self {
        MultiBuf { chunks, current: 0 }
    }
}

fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
    let chunk = &buf.chunks[buf.current];
    buf.current = (buf.current + 1) % buf.chunks.len();
    chunk
}           

3. Implement methods in C++

Next, we need to implement the corresponding functions in C++. Define the C++ interface in the demo/include/blobstore.h file:

// demo/include/blobstore.h

#pragma once
#include <cxx.h>

struct BlobMetadata {
    size_t size;
    rust::Vec<rust::String> tags;
};

struct BlobstoreClient;
std::unique_ptr<BlobstoreClient> new_blobstore_client();
uint64_t put(BlobstoreClient& self, rust::Box<MultiBuf>& parts);
void tag(BlobstoreClient& self, uint64_t blobid, rust::Str tag);
BlobMetadata metadata(const BlobstoreClient& self, uint64_t blobid);           

然后在demo/src/blobstore.cc中实现这些方法:

// demo/src/blobstore.cc

#include "demo/include/blobstore.h"
#include <memory>
#include <vector>

struct MultiBuf {
    std::vector<std::vector<uint8_t>> chunks;
    size_t current;

    MultiBuf(std::vector<std::vector<uint8_t>>&& chunks)
        : chunks(std::move(chunks)), current(0) {}
};

struct BlobstoreClient {
    std::vector<std::shared_ptr<MultiBuf>> buffers;
};

std::unique_ptr<BlobstoreClient> new_blobstore_client() {
    return std::make_unique<BlobstoreClient>();
}

uint64_t put(BlobstoreClient& self, rust::Box<MultiBuf>& parts) {
    self.buffers.push_back(std::shared_ptr<MultiBuf>(parts.release()));
    return self.buffers.size() - 1;
}

void tag(BlobstoreClient& self, uint64_t blobid, rust::Str tag) {
    // Implementation of tagging (not essential for this example)
}

BlobMetadata metadata(const BlobstoreClient& self, uint64_t blobid) {
    BlobMetadata metadata;
    metadata.size = self.buffers[blobid]->chunks.size();
    // Populate tags as needed
    return metadata;
}           

4. Main Program

Finally, we can use these methods in the main Rust program:

// src/main.rs

fn main() {
    let client = ffi::new_blobstore_client();
    let mut buf = MultiBuf::new(vec![vec![1, 2, 3], vec![4, 5, 6]]);
    
    let id = client.put(&mut buf);
    let meta = client.metadata(id);
    println!("Blob id: {}, size: {}", id, meta.size);
}           

Safety & Performance

CXX libraries are designed with security in mind as well as performance. For example, using static analysis prevents passing types from C++ to Rust that shouldn't be passed by value. In addition, CXX inserts zero-overhead workarounds when necessary, allowing the struct to be passed by value while maintaining ABI compatibility.

summary

CXX provides a secure and easy mechanism for interoperating between Rust and C++. This article shows how to configure the CXX library, define FFI boundaries, and implement the corresponding methods in Rust and C++. By using CXX, we can achieve efficient cross-language calls without sacrificing security.

Read on