Design decisions

Keep Rust code normal and idiomatic. All glue code should live in the C++ side.

One of the most important use of a C++/Rust interop tool like Zngur is in Rust rewrite projects. In those projects, most people are C++ experts but have little to no Rust experience. Writing glue codes in Rust and using UniquePtr, Pin and similar make Rust code more weirder and harder than what Rust actually is, creates a not really great first Rust experience for them.

Writing glue code in C++ also makes things considerably easier, since C++ semantics are a superset of Rust semantics (See idea) and Rust can't express all C++ things easily.

Keep main.zng in a separate file

CXX style embedding of the IDL in a Rust proc macro confused Rust newcomers, so Zngur avoids it.

Be a zero cost abstraction

When Rust and C++ are used in a project, it means that performance is a requirement. So, unlike interoperability tools between Rust and higher level languages, Zngur is not allowed to do deep copies or invisible allocations.

Be build system agnostic

C/C++ build systems are complex, each in a different way. To support all of them, Zngur doesn't integrate with any of them. Any build system that can do the following process is able to build a Zngur project:

  • Running zngur g main.zng
  • Building the Rust project (e.g. by running the cargo build)
  • Build the C++ generated.cpp together with the rest of codes
  • Link all together

Keep Rust things Rusty

  • To minimizing the surprise.
  • Rust decisions are usually superior to C++ ones.

Result<T, E> is not automatically converted to exception

Result<T, E> has some benefits over exception based error handling. For example, the unhappy case can not be forgotten and must be handled. Due these benefits, a similar std::expected<T, E> is added to the C++23. In order to not losing this Rust benefit, Result<T, E> is not converted to a C++ exception.

Panics, which are implemented by stack unwinding similar to C++ exceptions, are converted to a C++ exception with the #convert_panic_to_exception flag. So if you quickly want an exception out of a Result<T, E>, you can use .unwrap().

Copy constructors are deleted, manual .clone() should be used

Implicit copy constructors are a source of accidental performance cost, and complicate the control flow of program. Rust doesn't support them and uses explicit .clone() calls for that propose, and Zngur follows Rust.

Note that for Copy types, where the move operation is not destructive, Zngur doesn't delete the copy constructor.

RustType r; has the same semantics as let r: RustType;

Normally in C++ the default constructor creates a basic initialized object, and you can use it immediately after that. For example, this code is valid:

std::vector<int32_t> v;
v.push_back(2);

But in Zngur, default constructor always exists and creates an uninitialized object, so this code is invalid:

rust::std::vec::Vec<int32_t> v;
v.push(2);

An alternative would be running Default::default() in the default constructor. This behavior is selected over that, because it can enable somethings that are not possible without it with the same performance, such as conditional initialization:

Vec<int32_t> v;
if (reserve_capacity) {
    v = Vec<int32_t>::with_capacity(1000);
} else {
    v = Vec<int32_t>::new_();
}

If Vec<int32_t> v used the default constructor, it would be a waste call to itself and a wasted call to the drop code executed immediately after that. Rust also support this, but checks the initialization before usage, which Zngur can't check in the compile time, but will check in the run time by default.

Rust functions returning () return rust::Unit in C++ instead of void

This one has very little practical benefits, and might be revisited in future.