From 54a0edf2a9411b9f9c28e22a739ecbd0c1c75770 Mon Sep 17 00:00:00 2001 From: hkalbasi Date: Thu, 12 Oct 2023 20:08:51 +0330 Subject: [PATCH] Add a section for calling C++ from Rust in the tutorial --- Cargo.lock | 9 ++ book/src/tutorial.md | 260 ++++++++++++++++++++++++++++++ examples/tutorial/README.md | 2 +- examples/tutorial_cpp/Cargo.toml | 15 ++ examples/tutorial_cpp/README.md | 9 ++ examples/tutorial_cpp/build.rs | 33 ++++ examples/tutorial_cpp/impls.cpp | 52 ++++++ examples/tutorial_cpp/inventory.h | 31 ++++ examples/tutorial_cpp/main.zng | 59 +++++++ examples/tutorial_cpp/src/main.rs | 13 ++ zngur-generator/src/lib.rs | 4 +- zngur-generator/src/rust.rs | 17 +- 12 files changed, 499 insertions(+), 5 deletions(-) create mode 100644 examples/tutorial_cpp/Cargo.toml create mode 100644 examples/tutorial_cpp/README.md create mode 100644 examples/tutorial_cpp/build.rs create mode 100644 examples/tutorial_cpp/impls.cpp create mode 100644 examples/tutorial_cpp/inventory.h create mode 100644 examples/tutorial_cpp/main.zng create mode 100644 examples/tutorial_cpp/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 4538bcc..311e654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,6 +300,15 @@ version = "0.4.0" name = "example-tutorial" version = "0.4.0" +[[package]] +name = "example-tutorial_cpp" +version = "0.4.0" +dependencies = [ + "build-rs", + "cc", + "zngur", +] + [[package]] name = "expect-test" version = "1.4.1" diff --git a/book/src/tutorial.md b/book/src/tutorial.md index 3e5da8f..d64a2df 100644 --- a/book/src/tutorial.md +++ b/book/src/tutorial.md @@ -332,3 +332,263 @@ zngur_dbg(v); ``` You can see the full code at [`examples/tutorial`](https://github.com/HKalbasi/zngur/blob/main/examples/tutorial) + +## Calling C++ from Rust + +C++/Rust interop has two sides, and no interop tool is complete without supporting both. Here, we will do the reverse of the +above task, swapping the Rust and C++ rules. So, let's assume we have this C++ code: + +```C++ +#include +#include + +namespace cpp_inventory { +struct Item { + std::string name; + uint32_t size; +}; + +struct Inventory { + std::vector items; + uint32_t remaining_space; + + Inventory(uint32_t space) : items(), remaining_space(space) {} + + void add_item(Item item) { + remaining_space -= item.size; + items.push_back(std::move(item)); + } + + void add_banana(uint32_t count) { + add_item(Item{ + .name = "banana", + .size = 7, + }); + } +}; + +} // namespace cpp_inventory +``` + +Create a new cargo project, this time a binary one since we want to write the main function to live inside Rust. Copy the above code into +the `inventory.h` file. Then create a `main.zng` file with the following content: + +``` +type crate::Inventory { + #layout(size = 16, align = 8); + + constructor(ZngurCppOpaqueOwnedObject); + + #cpp_value "0" "::cpp_inventory::Inventory"; +} + +type crate::Item { + #layout(size = 16, align = 8); + + constructor(ZngurCppOpaqueOwnedObject); + + #cpp_value "0" "::cpp_inventory::Item"; +} +``` + +And add these to the `main.rs` file: + +```Rust +mod generated { + include!(concat!(env!("OUT_DIR"), "/generated.rs")); +} + +struct Inventory(generated::ZngurCppOpaqueOwnedObject); +struct Item(generated::ZngurCppOpaqueOwnedObject); +``` + +This time we will use the Zngur generator inside of cargo build script. We could still use the `zngur-cli` but in projects +where cargo is the boss, using build script is better. Add `zngur` and `cc` to your build dependencies: + +```toml +[build-dependencies] +cc = "1.0" +build-rs = "0.1.2" # This one is optional +zngur = "latest-version" +``` + +Then fill the `build.rs` file: + +```Rust +use std::{env, path::PathBuf}; + +use zngur::Zngur; + +fn main() { + build::rerun_if_changed("main.zng"); + build::rerun_if_changed("impls.cpp"); + + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + Zngur::from_zng_file(crate_dir.join("main.zng")) + .with_cpp_file(out_dir.join("generated.cpp")) + .with_h_file(out_dir.join("generated.h")) + .with_rs_file(out_dir.join("generated.rs")) + .generate(); + + let my_build = &mut cc::Build::new(); + let my_build = my_build + .cpp(true) + .compiler("g++") + .include(&crate_dir) + .include(&out_dir); + let my_build = || my_build.clone(); + + my_build() + .file(out_dir.join("generated.cpp")) + .compile("zngur_generated"); + my_build().file("impls.cpp").compile("impls"); +} +``` + +Now we have a `crate::Inventory` and a `crate::Item` that can contain their C++ counterparts. But there is no way to use +them in Rust. In Zngur, the Rust side can't access C++ opaque objects. So to make these types useful in Rust, we can +add `impl` blocks for these types in C++. Add this to the `main.zng`: + +``` +type str { + wellknown_traits(?Sized); + + fn as_ptr(&self) -> *const u8; + fn len(&self) -> usize; +} + +extern "C++" { + impl crate::Inventory { + fn new_empty(u32) -> crate::Inventory; + fn add_banana(&mut self, u32); + fn add_item(&mut self, crate::Item); + } + + impl crate::Item { + fn new(&str, u32) -> crate::Item; + } +} +``` + +Now we can define these methods in the C++ and use them in Rust. Create a file named `impls.cpp` with this content: + +```C++ +#include "generated.h" +#include + +using namespace rust::crate; + +Inventory rust::Impl::new_empty(uint32_t space) { + return Inventory( + rust::ZngurCppOpaqueOwnedObject::build(space)); +} + +rust::Unit rust::Impl::add_banana(rust::RefMut self, + uint32_t count) { + self.cpp().add_banana(count); + return {}; +} + +rust::Unit rust::Impl::add_item(rust::RefMut self, + Item item) { + self.cpp().add_item(item.cpp()); + return {}; +} + +Item rust::Impl::new_(rust::Ref name, uint32_t size) { + return Item(rust::ZngurCppOpaqueOwnedObject::build( + cpp_inventory::Item{ + .name = ::std::string(reinterpret_cast(name.as_ptr()), + name.len()), + .size = size})); +} +``` + +These functions look like some unnecessary boilerplate, but writing them has some benefits: + +- We can convert C++ types to the Rust equivalents in these functions. For example, converting a pointer and length to a slice, or `&str` to `std::string` that + happened in the `Item::new` above. +- We can convert exceptions to Rust `Result` or `Option`. +- We can control the signature of methods, and use proper lifetimes and mutability for references. In case of mutability, Rust mutability means + exclusiveness, which might be too restrictive and we may want to consider the C++ type interior mutable. We can also add nullability with `Option` or + make the function `unsafe`. +- We can choose Rusty names for the functions (like `new` and `len`) or put the functionality in the proper trait (for example implementing the + `Iterator` trait instead of exposing the `.begin` and `.end` functions) + +Even in the tools that support calling C++ functions directly, people often end up writing Rust wrappers around C++ types for +these reasons. In Zngur, that code is the wrapper, which lives in the C++ so it can do whatever C++ does. + +In the Rust to C++ side, we used `zngur_dbg` macro to see the result. We will do the same here with the `dbg!` macro. To do that, we need to implement +the `Debug` trait for `crate::Inventory`. Add this to the `main.zng`: + +``` +// ... + +type ::std::fmt::Result { + #layout(size = 1, align = 1); + + constructor Ok(()); +} + +type ::std::fmt::Formatter { + #layout(size = 64, align = 8); + + fn write_str(&mut self, &str) -> ::std::fmt::Result; +} + +extern "C++" { + // ... + + impl std::fmt::Debug for crate::Inventory { + fn fmt(&self, &mut ::std::fmt::Formatter) -> ::std::fmt::Result; + } +} +``` + +and this code to the `impls.cpp`: + +```C++ +rust::std::fmt::Result rust::Impl::fmt( + rust::Ref<::rust::crate::Inventory> self, + rust::RefMut<::rust::std::fmt::Formatter> f) { + ::std::string result = "Inventory { remaining_space: "; + result += ::std::to_string(self.cpp().remaining_space); + result += ", items: ["; + bool is_first = true; + for (const auto &item : self.cpp().items) { + if (!is_first) { + result += ", "; + } else { + is_first = false; + } + result += "Item { name: \""; + result += item.name; + result += "\", size: "; + result += ::std::to_string(item.size); + result += " }"; + } + result += "] }"; + return f.write_str(rust::Str::from_char_star(result.c_str())); +} +``` + +So now we can write the main function: + +```Rust +fn main() { + let mut inventory = Inventory::new_empty(1000); + inventory.add_banana(3); + inventory.add_item(Item::new("apple", 5)); + dbg!(inventory); +} +``` + +and run it: + +``` +[examples/tutorial_cpp/src/main.rs:12] inventory = Inventory { remaining_space: 974, items: [Item { name: "banana", size: 7 }, Item { name: "banana", size: 7 }, Item { name: "banana", size: 7 }, Item { name: "apple", size: 5 }] } +``` + +You can see the full code at [`examples/tutorial_cpp`](https://github.com/HKalbasi/zngur/blob/main/examples/tutorial_cpp) diff --git a/examples/tutorial/README.md b/examples/tutorial/README.md index ab4da0f..01b43f0 100644 --- a/examples/tutorial/README.md +++ b/examples/tutorial/README.md @@ -1,6 +1,6 @@ # Example: Tutorial -Full code of the [Tutorial](https://hkalbasi.github.io/zngur/tutorial.html) in the Zngur book. +Full code of the [Tutorial](https://hkalbasi.github.io/zngur/tutorial.html) part 1 (Calling Rust from C++) in the Zngur book. To run this example: diff --git a/examples/tutorial_cpp/Cargo.toml b/examples/tutorial_cpp/Cargo.toml new file mode 100644 index 0000000..35c70c7 --- /dev/null +++ b/examples/tutorial_cpp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example-tutorial_cpp" +version = "0.4.0" +edition.workspace = true +license.workspace = true +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[build-dependencies] +cc = "1.0" +build-rs = "0.1.2" +zngur = { path = "../../zngur" } diff --git a/examples/tutorial_cpp/README.md b/examples/tutorial_cpp/README.md new file mode 100644 index 0000000..d94a70e --- /dev/null +++ b/examples/tutorial_cpp/README.md @@ -0,0 +1,9 @@ +# Example: Tutorial Cpp + +Full code of the [Tutorial](https://hkalbasi.github.io/zngur/tutorial.html) part 2 (Calling C++ from Rust) in the Zngur book. + +To run this example: + +``` +cargo run +``` diff --git a/examples/tutorial_cpp/build.rs b/examples/tutorial_cpp/build.rs new file mode 100644 index 0000000..dcc2496 --- /dev/null +++ b/examples/tutorial_cpp/build.rs @@ -0,0 +1,33 @@ +use std::{env, path::PathBuf}; + +use zngur::Zngur; + +fn main() { + build::rerun_if_changed("main.zng"); + build::rerun_if_changed("impls.cpp"); + build::rerun_if_env_changed("CXX"); + + let cxx = env::var("CXX").unwrap_or("c++".to_owned()); + + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + Zngur::from_zng_file(crate_dir.join("main.zng")) + .with_cpp_file(out_dir.join("generated.cpp")) + .with_h_file(out_dir.join("generated.h")) + .with_rs_file(out_dir.join("generated.rs")) + .generate(); + + let my_build = &mut cc::Build::new(); + let my_build = my_build + .cpp(true) + .compiler(&cxx) + .include(&crate_dir) + .include(&out_dir); + let my_build = || my_build.clone(); + + my_build() + .file(out_dir.join("generated.cpp")) + .compile("zngur_generated"); + my_build().file("impls.cpp").compile("impls"); +} diff --git a/examples/tutorial_cpp/impls.cpp b/examples/tutorial_cpp/impls.cpp new file mode 100644 index 0000000..7b33e64 --- /dev/null +++ b/examples/tutorial_cpp/impls.cpp @@ -0,0 +1,52 @@ +#include "generated.h" +#include + +using namespace rust::crate; + +Inventory rust::Impl::new_empty(uint32_t space) { + return Inventory( + rust::ZngurCppOpaqueOwnedObject::build(space)); +} + +rust::Unit rust::Impl::add_banana(rust::RefMut self, + uint32_t count) { + self.cpp().add_banana(count); + return {}; +} + +rust::Unit rust::Impl::add_item(rust::RefMut self, + Item item) { + self.cpp().add_item(item.cpp()); + return {}; +} + +Item rust::Impl::new_(rust::Ref name, uint32_t size) { + return Item(rust::ZngurCppOpaqueOwnedObject::build( + cpp_inventory::Item{ + .name = ::std::string(reinterpret_cast(name.as_ptr()), + name.len()), + .size = size})); +} + +rust::std::fmt::Result rust::Impl::fmt( + rust::Ref<::rust::crate::Inventory> self, + rust::RefMut<::rust::std::fmt::Formatter> f) { + ::std::string result = "Inventory { remaining_space: "; + result += ::std::to_string(self.cpp().remaining_space); + result += ", items: ["; + bool is_first = true; + for (const auto &item : self.cpp().items) { + if (!is_first) { + result += ", "; + } else { + is_first = false; + } + result += "Item { name: \""; + result += item.name; + result += "\", size: "; + result += ::std::to_string(item.size); + result += " }"; + } + result += "] }"; + return f.write_str(rust::Str::from_char_star(result.c_str())); +} diff --git a/examples/tutorial_cpp/inventory.h b/examples/tutorial_cpp/inventory.h new file mode 100644 index 0000000..7471439 --- /dev/null +++ b/examples/tutorial_cpp/inventory.h @@ -0,0 +1,31 @@ +#include +#include +#include + +namespace cpp_inventory { +struct Item { + std::string name; + uint32_t size; +}; + +struct Inventory { + std::vector items; + uint32_t remaining_space; + Inventory(uint32_t space) : items(), remaining_space(space) {} + + void add_item(Item item) { + remaining_space -= item.size; + items.push_back(std::move(item)); + } + + void add_banana(uint32_t count) { + for (uint32_t i = 0; i < count; i += 1) { + add_item(Item{ + .name = "banana", + .size = 7, + }); + } + } +}; + +} // namespace cpp_inventory diff --git a/examples/tutorial_cpp/main.zng b/examples/tutorial_cpp/main.zng new file mode 100644 index 0000000..2422311 --- /dev/null +++ b/examples/tutorial_cpp/main.zng @@ -0,0 +1,59 @@ +#cpp_additional_includes " + #include +" + +type () { + #layout(size = 0, align = 1); + wellknown_traits(Copy); +} + +type str { + wellknown_traits(?Sized); + + fn as_ptr(&self) -> *const u8; + fn len(&self) -> usize; +} + +type crate::Inventory { + #layout(size = 16, align = 8); + + constructor(ZngurCppOpaqueOwnedObject); + + #cpp_value "0" "::cpp_inventory::Inventory"; +} + +type crate::Item { + #layout(size = 16, align = 8); + + constructor(ZngurCppOpaqueOwnedObject); + + #cpp_value "0" "::cpp_inventory::Item"; +} + +type ::std::fmt::Result { + #layout(size = 1, align = 1); + + constructor Ok(()); +} + +type ::std::fmt::Formatter { + #layout(size = 64, align = 8); + + fn write_str(&mut self, &str) -> ::std::fmt::Result; +} + +extern "C++" { + impl crate::Inventory { + fn new_empty(u32) -> crate::Inventory; + fn add_banana(&mut self, u32); + fn add_item(&mut self, crate::Item); + } + + impl crate::Item { + fn new(&str, u32) -> crate::Item; + } + + impl std::fmt::Debug for crate::Inventory { + fn fmt(&self, &mut ::std::fmt::Formatter) -> ::std::fmt::Result; + } +} diff --git a/examples/tutorial_cpp/src/main.rs b/examples/tutorial_cpp/src/main.rs new file mode 100644 index 0000000..5e124de --- /dev/null +++ b/examples/tutorial_cpp/src/main.rs @@ -0,0 +1,13 @@ +mod generated { + include!(concat!(env!("OUT_DIR"), "/generated.rs")); +} + +struct Inventory(generated::ZngurCppOpaqueOwnedObject); +struct Item(generated::ZngurCppOpaqueOwnedObject); + +fn main() { + let mut inventory = Inventory::new_empty(1000); + inventory.add_banana(3); + inventory.add_item(Item::new("apple", 5)); + dbg!(inventory); +} diff --git a/zngur-generator/src/lib.rs b/zngur-generator/src/lib.rs index 661fe50..e61d24a 100644 --- a/zngur-generator/src/lib.rs +++ b/zngur-generator/src/lib.rs @@ -76,7 +76,7 @@ impl ZngurGenerator { }, }); cpp_methods.push(CppMethod { - name: format!("matches_{}", cpp_handle_keyword(&name)), + name: format!("matches_{}", name), kind: ZngurMethodReceiver::Ref(Mutability::Not), sig: CppFnSig { rust_link_name: rust_link_names.match_check, @@ -220,7 +220,7 @@ impl ZngurGenerator { .map(|(method, link_name)| { let (_, inputs) = real_inputs_of_method(method, &impl_block.ty); ( - method.name.clone(), + cpp_handle_keyword(&method.name).to_owned(), CppFnSig { rust_link_name: link_name.clone(), inputs, diff --git a/zngur-generator/src/rust.rs b/zngur-generator/src/rust.rs index 78afccb..e397d41 100644 --- a/zngur-generator/src/rust.rs +++ b/zngur-generator/src/rust.rs @@ -138,8 +138,6 @@ impl Default for RustFile { fn default() -> Self { Self { text: r#" -#![allow(non_snake_case)] - #[allow(dead_code)] mod zngur_types { pub struct ZngurCppOpaqueBorrowedObject(()); @@ -320,6 +318,7 @@ impl RustFile { wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {mangled_name}( data: *mut u8, @@ -380,6 +379,7 @@ pub extern "C" fn {mangled_name}( wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {mangled_name}( data: *mut u8, @@ -438,6 +438,7 @@ pub extern "C" fn {mangled_name}( wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {mangled_name}( data: *mut u8, @@ -482,6 +483,7 @@ pub extern "C" fn {mangled_name}( w!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {constructor}("# ); @@ -500,6 +502,7 @@ pub extern "C" fn {constructor}("# w!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {match_check}(i: *mut u8, o: *mut u8) {{ unsafe {{ *o = matches!(&*(i as *mut &_), {rust_name} {{ .. }}) as u8; @@ -613,6 +616,7 @@ pub(crate) fn {rust_name}("# w!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {mangled_name}(d: *mut u8) -> *mut ZngurCppOpaqueOwnedObject {{ unsafe {{ &mut (*(d as *mut {ty})).{field} }} @@ -637,6 +641,7 @@ pub extern "C" fn {mangled_name}(d: *mut u8) -> *mut ZngurCppOpaqueOwnedObject { w!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {mangled_name}("# ); @@ -681,6 +686,7 @@ pub extern "C" fn {mangled_name}("# wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {drop_in_place}(v: *mut u8) {{ unsafe {{ ::std::ptr::drop_in_place(v as *mut {ty}); @@ -694,6 +700,7 @@ pub extern "C" fn {drop_in_place}(v: *mut u8) {{ unsafe {{ wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {pretty_print}(v: *mut u8) {{ eprintln!("{{:#?}}", unsafe {{ &*(v as *mut {ty}) }}); @@ -702,6 +709,7 @@ pub extern "C" fn {pretty_print}(v: *mut u8) {{ wln!( self, r#" +#[allow(non_snake_case)] #[no_mangle] pub extern "C" fn {debug_print}(v: *mut u8) {{ eprintln!("{{:?}}", unsafe {{ &*(v as *mut {ty}) }}); @@ -721,6 +729,7 @@ pub extern "C" fn {debug_print}(v: *mut u8) {{ r#"thread_local! {{ pub static PANIC_PAYLOAD: ::std::cell::Cell> = ::std::cell::Cell::new(None); }} + #[allow(non_snake_case)] #[no_mangle] pub fn __zngur_detect_panic() -> u8 {{ PANIC_PAYLOAD.with(|p| {{ @@ -731,6 +740,7 @@ pub extern "C" fn {debug_print}(v: *mut u8) {{ }}) }} + #[allow(non_snake_case)] #[no_mangle] pub fn __zngur_take_panic() {{ PANIC_PAYLOAD.with(|p| {{ @@ -772,16 +782,19 @@ pub extern "C" fn {debug_print}(v: *mut u8) {{ wln!( self, r#" + #[allow(non_snake_case)] #[no_mangle] pub fn {size_fn}() -> usize {{ ::std::mem::size_of::<{ty}>() }} + #[allow(non_snake_case)] #[no_mangle] pub fn {alloc_fn}() -> *mut u8 {{ unsafe {{ ::std::alloc::alloc(::std::alloc::Layout::new::<{ty}>()) }} }} + #[allow(non_snake_case)] #[no_mangle] pub fn {free_fn}(p: *mut u8) {{ unsafe {{ ::std::alloc::dealloc(p, ::std::alloc::Layout::new::<{ty}>()) }}