Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up


Find what you need - explore our website and developer resources
5 March 2026
With Rust consistently proving itself as a good replacement for C++ due to its type system, memory safety, and macros, many industry giants are starting to write or rewrite things in this more modern systems language.
That being said, the maturity, and broad ecosystem of C++ cannot be understated, and many companies have decades of code written in established C++ tech stacks, for example the Qt framework. This is where interoperability comes in, allowing Rust's modern features and package management to be paired with existing C++ knowledge and codebases, or call rigorously tested and optimised C++ libraries in a new Rust codebase.
Now that CXX - the most widely adopted Rust with C++ interop library - is mature and kept very stable, we would like to explore Zngur, another emerging library in this space.
One important differentiator between these libraries is their aims, with Zngur attempting to support a larger subset of semantics, with the cost of some performance, ergonomics and safety, whereas CXX preaches safety and a reduced feature set, with the claim of no overhead. As stated in the Zngur documentation:
"Zngur also makes safety guarantees but also tries to be powerful enough to handle arbitrary signatures from Rust code"
One example where Zngur is less restrictive than CXX is that it permits owned objects across the FFI boundary, using the #cpp_value directive. Zngur also avoids guarantees around Send and Sync as we will discuss in the multithreading section.
I personally found Zngur a little easier to set up, but it does have a more convoluted way of specifying the language boundary, which is explained below. Additionally, Zngur aims to keep most of the generated and glue code in the C++ side, as opposed to splitting it over Rust and C++, and this can potentially make it easier to debug, but also eschews some of Rust's safety guarantees. On the other hand, the many thousands of lines of C++ code can sometimes be redundant and difficult to search through.
Now let's get into the nitty-gritty details of this comparison that separate these two frameworks. Zngur poses some advantages over CXX with more arbitrary container support, and trait implementations with dynamic dispatch. On the other hand, CXX has a more solid build setup, much larger community, and external support for async Rust, all of which we will explore below.
One of Zngur's selling points is its claim to support most container types with "almost full API", whereas CXX has a more limited selection. Some of these types include nesting vectors such as Vec<Vec<T>>, HashMap<K, V>, and Arc<dyn T> (we will discuss trait objects next).
This first class support comes with a cost, however, as each type needs to be manually specified, e.g. Vec<Vec<i32>> and Vec<Vec<String>> need to be separately declared (and even String needs to be declared). This quickly compounds, as for each type, you must specify all std library methods you want to use, which can lead to very large files.
CXX has good support for many container types with "out of the box" use, and more complex types can be wrapped with the newtype pattern, and used opaquely, with slightly reduced ergonomics. CXX also has the very powerful feature of being able to automatically generate the appropriate specializations for the types you need. In most cases, CXX has enough support for standard containers - and makes using them a lot simpler - but if your app is going to have a lot of complex types, you may be better going with Zngur.
Here we have an example IDL (interface description language) file declaring a 2D vector. Zngur uses separate files for it's boundary description, whereas CXX has the bridge macro, but these perform the same role.
// Exposes nested vector types
mod vec {
type Vec<i32> {
#layout(size = 24, align = 8);
wellknown_traits(Debug);
fn new() -> Vec<i32>;
fn push(&mut self, i32);
fn len(&self) -> usize;
}
type Vec<Vec<i32>> {
#layout(size = 24, align = 8);
wellknown_traits(Debug);
fn new() -> Vec<Vec<i32>>;
fn push(&mut self, Vec<i32>);
fn len(&self) -> usize;
}
} We can construct, move, and call functions with the type in C++ like
auto row1 = to_vec({1,0,0});
auto row2 = to_vec({0,2,0});
auto row3 = to_vec({0,0,1});
auto matrix1 = Vec<Vec<int32_t>>::new_();
auto matrices = Vec<Vec<Vec<int32_t>>>::new_();
matrix1.push(std::move(row1));
matrix1.push(std::move(row2));
matrix1.push(std::move(row3));
matrices.push(std::move(matrix1));
zngur_dbg(rust::crate::determinants(matrices)); The template has been declared in C++ to shorten rust::std::vec::Vec<T>, and the to_vec helper is used so rows can be created with initializer lists.
The Rust function is simply calculating matrix determinants, and performing this on a vector of matrices. This way we make use of 1D, 2D and 3D vectors across the boundary.
/// Treating nested vector like a 3x3 matrix, returns list of
pub fn determinants(matrices: &Vec<Vec<Vec<i32>>>) -> Vec<i32> {
let mut result = vec![];
for m in matrices {
// Naming components to make formula easier to read
let a = m[0][0];
// ...
let i = m[2][2];
let det = a*e*i + b*f*g + c*d*h - (c*e*g + a*f*h + b*d*i);
result.push(det);
}
result
} CXX does not support bridging arbitrary containers, but this can easily be remedied using the newtype pattern. Although they must then be opaque, you can put more complex containers behind a struct, and pass that over the boundary, like so:
// We define a newtype, and can give it some basic methods
#[derive(Debug)]
struct Matrix {
inner: Vec<Vec<i32>>,
}
impl Matrix {
fn get_row(&self, row: usize) -> &Vec<i32> {
&self.inner[row]
}
} And then adding some C++ code, we can use this newtype to perform some calculations
// We use an app class to hold our methods
ExampleApp::ExampleApp() {}
std::unique_ptr<ExampleApp> new_app() {
return std::unique_ptr<ExampleApp>(new ExampleApp());
}
// Calculating the determinants the same as the Zngur example
int32_t ExampleApp::calculate_determinant(const Matrix &m) const {
auto row0 = m.get_row(0);
auto row1 = m.get_row(1);
auto row2 = m.get_row(2);
// We can use the index operator, because CXX maps Rust Vec to C++ vector, something not possible in Zngur
auto a = row0[0];
// ...
auto i = row2[2];
auto det = a*e*i + b*f*g + c*d*h - (c*e*g + a*f*h + b*d*i);
return det;
} We then specify the boundary using the CXX bridge macro:
#[cxx::bridge]
mod ffi {
extern "Rust" {
type Matrix;
fn get_row(self: &Matrix, row: usize) -> &Vec<i32>;
}
unsafe extern "C++" {
include!("cxx-example/include/example.h");
type ExampleApp;
fn new_app() -> UniquePtr<ExampleApp>;
fn calculate_determinant(&self, matrix: &Matrix) -> i32;
}
} This can then be called like so in our main function:
fn main() {
let app = ffi::new_app();
let m = vec![vec![1, 2, 3], vec![4, 5, 0], vec![0, 0, 9]];
let mat = Matrix { inner: m };
let det = app.calculate_determinant(&mat);
} Although similar, both approaches have their strengths and weaknesses.
Whilst CXX has support for basic Rust enums, Zngur's built-in support for tagged union type enums (variants that contain data) is very powerful for Rust users.
This means that defining Option<T> and Result<T, E> in your crate is trivial, and these are two of the most used data types in Rust. This includes constructors, e.g. instantiating Ok(value) in C++ code. Of course, the developer still needs to specify that they want to use these types in the IDL file.
On the other hand, the CXX community has a solution for this, in that if you need support for more than basic enums, there exists cxx-enumext, which provides the support for much more complex enums, including data with lifetimes, and boxed data. There is certainly something to be said for Zngur's wider support for declaring these algebraic data types, however.
Another strength of Zngur's is implementing traits in C++. For example, as a replacement for abstract classes or interfaces, you can define a Rust trait, and implement it in C++, for either Rust or C++ types, allowing polymorphism over the boundary via constructing a Box<dyn MyTrait>.
For example, as a replacement for abstract classes or interfaces, you have two options when it comes to traits:
One caveat to this, however, is that Zngur provides a make_box C++ helper for using these objects polymorphically, but is only accessible to C++ classes that inherit from a trait, not Rust objects with Rust or C++ implementations. This does, however, seem like something that could be easily rectified in the future. Our workaround for this, is simply adding a method to your trait like so:
trait MyTrait {
// Your trait methods here
fn as_my_trait(self) -> Box<dyn MyTrait> where Self: Sized + 'static {
Box::new(self)
}
} In CXX this type of behaviour would be done via a UniquePtr instead and a C++ base class. This has its benefits and tradeoffs, but the first class support for trait objects in Zngur bodes well for leveraging the power that traits bring. Moreover, C++ lambdas can be turned into Box<dyn Fn> objects, allowing them to be passed to Rust functions like map like a Rust closure.
With this IDL definition:
mod crate {
// Declaring the methods we want to use in the trait
trait MediaType {
fn author(&self) -> ::std::string::String;
fn title(&self) -> ::std::string::String;
fn rating(&self) -> u8;
fn media_type(&self) -> ::std::string::String;
}
// Box type must be declared too
type Box<dyn crate::MediaType> {
#layout(size = 16, align = 8);
}
// A rust function to display Movies, Songs, Albums, and any new media types
// Simply prints out the author, title and rating in a pretty format
fn display_media(Box<dyn MediaType>);
type Movie {
#layout(size = 56, align = 8);
wellknown_traits(Debug);
constructor {author_name: ::std::string::String, movie_title: ::std::string::String, movie_rating: u8};
fn as_boxed(self) -> Box<dyn MediaType> use MediaType; // The use keyword specifies this method comes from the trait
field author_name (offset = 0, type = ::std::string::String);
field movie_title (offset = 24, type = ::std::string::String);
field movie_rating (offset = 48, type = u8);
}
type Song {
#layout(size = 56, align = 8);
wellknown_traits(Debug);
constructor {author: ::std::string::String, title: ::std::string::String, rating: u8};
field author (offset = 0, type = ::std::string::String);
field title (offset = 24, type = ::std::string::String);
field rating (offset = 48, type = u8);
}
}
// This block declares that we are going to implement the trait using C++
// This is done using the Impl template provided by Zngur
extern "C++" {
impl crate::MediaType for crate::Movie {
fn author(&self) -> ::std::string::String;
fn title(&self) -> ::std::string::String;
fn rating(&self) -> u8;
fn media_type(&self) -> ::std::string::String;
}
} We can implement traits for our Rust types using C++ like so
// Trait impls
String
rust::Impl<Movie, MediaType>::author(
rust::Ref<Movie> self) {
// String have to be clone to be owned in C++ since they are allocated by the rust heap, so cannot be moved easily
return self.author_name.clone();
}
String
rust::Impl<Movie, MediaType>::title(
rust::Ref<Movie> self) {
return self.movie_title.clone();
}
String
rust::Impl<Movie, MediaType>::media_type(
rust::Ref<Movie> _self) {
return "Cinema"_rs.to_owned();
}
uint8_t
rust::Impl<Movie, MediaType>::rating(
rust::Ref<Movie> self) {
return self.movie_rating;
} We can then access boxed trait objects in a few ways:
// Implementing MediaType for rust::Song
class SongMedia : public MediaType {
Song song;
public:
SongMedia(Song s) : song(std::move(s)) {}
...
};
...
// Create an instance from a rust::Song
auto song_media = SongMedia(std::move(song));
// Call its methods
auto artist = song_media.author();
zngur_dbg(artist);
// Get a boxed trait object using the Zngur helper
auto boxed_song_media = Box<Dyn<MediaType>>::make_box<SongMedia>(std::move(song_media));
display_media(std::move(boxed_song_media)); // Using the rust as_boxed method as shown above (called as_boxed here)
auto my_album_as_media_type = my_album.as_boxed();
display_media(std::move(my_album_as_media_type)); // display_media will use the C++ implentations, but note as_boxed comes from the trait in Rust.
auto my_movie_as_media_type = jaws.as_boxed();
display_media(std::move(my_movie_as_media_type)); Note that the trait contains an implemented method as_boxed, so C++ implementations can use it: fn as_boxed(self) -> Box<dyn MediaType> where Self: Sized + 'static { Box::new(self) }
These 3 options permit flexible use of traits implemented in Rust or C++.
In CXX, we use a different idiom, defining a base class, and then using UniquePtr to get access to it in Rust.
// Abstract base / interface
class Media {
public:
virtual rust::String media_type() const = 0;
virtual rust::String author() const = 0;
virtual rust::String title() const = 0;
virtual int rating() const = 0;
const Media& as_media() const { return *this; }
}; This acts as our "trait" since we need to override the virtual methods, and the as_media method is available on inheritors. It use the dereference in the return so that it can be coerced into the base class.
We can then define our inheriting classes:
class Movie : public Media {
public:
Movie(rust::String title, rust::String author, int rating) : m_title(std::move(title)), m_author(std::move(author)), m_rating(rating) {}
rust::String author() const override {
return m_author;
}
rust::String title() const override {
return m_title;
}
rust::String media_type() const override {
return rust::String("Movie");
}
int rating() const override {
return m_rating;
}
private:
rust::String m_title;
rust::String m_author;
int m_rating;
}; And do the same for our Song class.
We can then define our methods to obtain a unique pointer:
std::unique_ptr<Movie> make_movie(rust::String title, rust::String author, int rating) {
return std::make_unique<Movie>(title, author, rating);
}
std::unique_ptr<Song> make_song(rust::String title, rust::String author, int rating) {
return std::make_unique<Song>(title, author, rating);
}
// function to print all media types in a readable manner
void display_media(const Media& m) {
...
} We can then expose them in the bridge of the main file like so:
#[cxx::bridge]
mod ffi {
unsafe extern "C++" {
include!("cxx-example/include/media.h");
type Media;
type Movie;
type Song;
fn make_movie(title: String, author: String, rating: i32) -> UniquePtr<Movie>;
fn make_song(title: String, author: String, rating: i32) -> UniquePtr<Song>;
fn as_media(self: &Movie) -> &Media;
fn as_media(self: &Song) -> &Media;
fn display_media(m: &Media);
}
} And use them in main,
fn main() {
// Media
let jaws = make_movie("Jaws".to_owned(), "Steven Spielberg".to_owned(), 8);
let fur_elise = make_song("Fur Elise".to_owned(), "Mozart".to_owned(), 5);
display_media(jaws.as_ref().unwrap().as_media());
display_media(fur_elise.as_ref().unwrap().as_media());
} On the other side of the coin, CXX has support for async rust planned, allowing async functions to be simply declared in the bridge, but currently they offer a workaround for calling async functions via a new type struct, containing a oneshot channel (futures crate). Furthermore, the cxx-async crate adds support for async tasks, and is fairly well established, so it can be a robust addition to your project if you need async.
Zngur on the other hand does not have this support planned, and makes no mention of how to implement it yourself. Zngur also doesn't do any checks, however, around Send and Sync - despite having support for Arc<T> (a smart pointer used in threaded situations), as opposed to CXX's static assertions, which make multithreaded situations much safer.
A further benefit of CXX is its significantly higher adoption - being used in Chromium and Android - as well as its better written and more complete documentation. We did find that Zngur had a slightly simpler project setup, being that you can simply generate the glue code, then use your existing C++ build setup, along with cargo to generate a library, or alternatively compile a C++ library, and link to your Rust binary. This independence from a specific build system makes it quick to integrate with what your already have. It also has the added benefit of not requiring any dependencies, simply being a CLI tool which generates the glue code for you.
As we have often seen with CXX throughout the blog, this functionality does exist in some form, namely the cxxbridge command. This allows you to take a CXX bridge file, and manually generate the glue code.
For Cargo builds, CXX recommends cxx-build to obtain a CC builder, and the Zngur crate offers a builder struct to provide a similar CC interface, for building the glue code. Despite being harder to set up than the command line system, this offers deep configuration for more advanced use cases and could even allow combining Zngur with CXX. Corrosion also has experimental helpers for CXX builds with CMake (but there are no blockers on Zngur being supported here too in the future). Using a makefile or justfile to invoke build commands can also simplify the build process.
An example Zngur build script for a simple project might look like this:
use zngur::Zngur;
fn main() {
build::rerun_if_changed("main.zng");
build::rerun_if_changed("prelude.zng");
build::rerun_if_changed("trait_objects.zng");
build::rerun_if_changed("nested_types.zng");
build::rerun_if_changed("main.cpp");
build::rerun_if_changed("nested_types.rs");
build::rerun_if_changed("trait_objects.rs");
let crate_dir = build::cargo_manifest_dir();
// Generate glue including the header, implementations (generated.cpp) and Rust code.
Zngur::from_zng_file(crate_dir.join("main.zng"))
.with_cpp_file(crate_dir.join("generated.cpp"))
.with_h_file(crate_dir.join("generated.h"))
.with_rs_file(crate_dir.join("./src/generated.rs"))
.generate();
let my_build = &mut cc::Build::new();
let my_build = my_build.cpp(true).std("c++17");
let my_build = || my_build.clone();
// Build the C++ code using the CC builder, including the entrypoint to the program, and the generated impls
my_build().file("generated.cpp").compile("generated");
my_build().file("main.cpp").compile("main");
} In CXX a build script looks very similar, since they both use the CC builder interface:
fn main() {
cxx_build::bridge("src/main.rs")
.file("src/example.cc")
.compile("cxx-example");
println!("cargo:rerun-if-changed=src/example.cc");
println!("cargo:rerun-if-changed=include/example.h");
println!("cargo:rerun-if-changed=include/media.h");
} As it stands at the minute, CXX definitely has stronger documentation, examples, and adoption, but remains to have strict rules around what gets added, and slow maintenance. Zngur offers a few benefits, but lacks key documentation, references of supported items, or even syntax. It does pose some interesting ideas which could be adopted by CXX, however, such as trait objects, and tagged unions (Rust enums which contain data, for example Option and Result). Although Zngur may be more time-consuming to write due to repeated definitions, we provide a custom prelude which you can place in your project, with any necessary basic types you may want, and this also includes a tool to generate any combination of these that you may want, see our tool zngur-prelude for more.
Some key differences in what is supported are laid out below:
| Feature | CXX | Zngur |
|---|---|---|
| Generic Support | Limited to built in types (listed below) when trying to define new generic types, e.g. no support for HashMap or Vec<Vec<T>> without a newtype wrapper. | Very broad support for defining Vec<Vec<T>>, HashMap<K, V>, Arc<dyn Trait>, with the caveat that each specialization needs to be defined manually. |
| Built In Support | CXX has built in support for Vec, String, UniquePtr, &[T] and a few others, and has strong guarantees about them. | Very limited built in support, not even supporting String or bool out of the box, but has very broad support for specifying these types yourself. |
| Trait Objects | No direct support for Box<dyn Trait>, Docs recommend using UniquePtr instead | Full support: C++ types can implement Rust traits, and trait objects can be passed over the boundary. |
| Async Support | Partial via cxx-async; integrates with Rust Future including futures crate. | None yet. Can only emulate async via passing boxed function pointers and via channels. |
| Thread Safety | Enforces Send and Sync requirements via static assertions. | No assertions, but Mutex, Arc are supported. |
| Build System | Integrated via cxx_build crate to support Cargo, CMake, and Bazel builds. Also has the cxxbridge tool to generate glue code from a file | CLI tool to generate glue code (zngur g file.zng) then manual build. Build scripts are supported but less so, with lesser adoption. |
| Documentation Status | Mature, comprehensive docs at cxx.rs. | Sparse and incomplete, examples can be found in GitHub but they are limited. |
| Adoption | Widely used, including in large projects like Chromium and Android. | Early adoption, Adobe seems to have taken an interest. |
| Overhead | Zero-overhead abstraction, no serialization or copying. | Slight overhead possible; prioritizes flexibility and expressiveness. |
| Language Definition | Inline within #[cxx::bridge] macro in the main Rust file. | Specified in an IDL file (.zng). |
| Best Use | Stable, performant FFI, integration with CXX-Qt, projects with simple types at the boundary | Research, quick prototyping, or projects making strong use of Traits and dynamic dispatch in Rust. Easier to integrate into an existing C++ project |
Choose CXX if your team needs proven safety, documentation, and tooling integration. Choose Zngur if you’re exploring advanced polymorphism or container-heavy data flows and are comfortable managing ABI stability yourself.
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Learn Rust
In collaboration with our partners Ferrous Systems, KDAB provides a variety of introductory and advanced training courses for the Rust language.
Learn more
Learn Modern C++
Our hands-on Modern C++ training courses are designed to quickly familiarize newcomers with the language. They also update professional C++ developers on the latest changes in the language and standard library introduced in recent C++ editions.
Learn more