• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1{{#title Tutorial — Rust ♡ C++}}
2# Tutorial: CXX blobstore client
3
4This example walks through a Rust application that calls into a C++ client of a
5blobstore service. In fact we'll see calls going in both directions: Rust to C++
6as well as C++ to Rust. For your own use case it may be that you need just one
7of these directions.
8
9All of the code involved in the example is shown on this page, but it's also
10provided in runnable form in the *demo* directory of
11<https://github.com/dtolnay/cxx>. To try it out directly, run `cargo run` from
12that directory.
13
14This tutorial assumes you've read briefly about **shared structs**, **opaque
15types**, and **functions** in the [*Core concepts*](concepts.md) page.
16
17## Creating the project
18
19We'll use Cargo, which is the build system commonly used by open source Rust
20projects. (CXX works with other build systems too; refer to chapter 5.)
21
22Create a blank Cargo project: `mkdir cxx-demo`; `cd cxx-demo`; `cargo init`.
23
24Edit the Cargo.toml to add a dependency on the `cxx` crate:
25
26```toml,hidelines
27## Cargo.toml
28# [package]
29# name = "cxx-demo"
30# version = "0.1.0"
31# edition = "2018"
32
33[dependencies]
34cxx = "1.0"
35```
36
37We'll revisit this Cargo.toml later when we get to compiling some C++ code.
38
39## Defining the language boundary
40
41CXX relies on a description of the function signatures that will be exposed from
42each language to the other. You provide this description using `extern` blocks
43in a Rust module annotated with the `#[cxx::bridge]` attribute macro.
44
45We'll open with just the following at the top of src/main.rs and walk through
46each item in detail.
47
48```rust,noplayground
49// src/main.rs
50
51#[cxx::bridge]
52mod ffi {
53
54}
55#
56# fn main() {}
57```
58
59The contents of this module will be everything that needs to be agreed upon by
60both sides of the FFI boundary.
61
62## Calling a C++ function from Rust
63
64Let's obtain an instance of the C++ blobstore client, a class `BlobstoreClient`
65defined in C++.
66
67We'll treat `BlobstoreClient` as an *opaque type* in CXX's classification so
68that Rust does not need to assume anything about its implementation, not even
69its size or alignment. In general, a C++ type might have a move-constructor
70which is incompatible with Rust's move semantics, or may hold internal
71references which cannot be modeled by Rust's borrowing system. Though there are
72alternatives, the easiest way to not care about any such thing on an FFI
73boundary is to require no knowledge about a type by treating it as opaque.
74
75Opaque types may only be manipulated behind an indirection such as a reference
76`&`, a Rust `Box`, or a `UniquePtr` (Rust binding of `std::unique_ptr`). We'll
77add a function through which C++ can return a `std::unique_ptr<BlobstoreClient>`
78to Rust.
79
80```rust,noplayground
81// src/main.rs
82
83#[cxx::bridge]
84mod ffi {
85    unsafe extern "C++" {
86        include!("cxx-demo/include/blobstore.h");
87
88        type BlobstoreClient;
89
90        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
91    }
92}
93
94fn main() {
95    let client = ffi::new_blobstore_client();
96}
97```
98
99The nature of `unsafe` extern blocks is clarified in more detail in the
100[*extern "C++"*](extern-c++.md) chapter. In brief: the programmer is **not**
101promising that the signatures they have typed in are accurate; that would be
102unreasonable. CXX performs static assertions that the signatures exactly match
103what is declared in C++. Rather, the programmer is only on the hook for things
104that C++'s semantics are not precise enough to capture, i.e. things that would
105only be represented at most by comments in the C++ code. In this case, it's
106whether `new_blobstore_client` is safe or unsafe to call. If that function said
107something like "must be called at most once or we'll stomp yer memery", Rust
108would instead want to expose it as `unsafe fn new_blobstore_client`, this time
109inside a safe `extern "C++"` block because the programmer is no longer on the
110hook for any safety claim about the signature.
111
112If you build this file right now with `cargo build`, it won't build because we
113haven't written a C++ implementation of `new_blobstore_client` nor instructed
114Cargo about how to link it into the resulting binary. You'll see an error from
115the linker like this:
116
117```console
118error: linking with `cc` failed: exit code: 1
119 |
120 = /bin/ld: target/debug/deps/cxx-demo-7cb7fddf3d67d880.rcgu.o: in function `cxx_demo::ffi::new_blobstore_client':
121   src/main.rs:1: undefined reference to `cxxbridge1$new_blobstore_client'
122   collect2: error: ld returned 1 exit status
123```
124
125## Adding in the C++ code
126
127In CXX's integration with Cargo, all #include paths begin with a crate name by
128default (when not explicitly selected otherwise by a crate; see
129`CFG.include_prefix` in chapter 5). That's why we see
130`include!("cxx-demo/include/blobstore.h")` above &mdash; we'll be putting the
131C++ header at relative path `include/blobstore.h` within the Rust crate. If your
132crate is named something other than `cxx-demo` according to the `name` field in
133Cargo.toml, you will need to use that name everywhere in place of `cxx-demo`
134throughout this tutorial.
135
136```cpp
137// include/blobstore.h
138
139#pragma once
140#include <memory>
141
142class BlobstoreClient {
143public:
144  BlobstoreClient();
145};
146
147std::unique_ptr<BlobstoreClient> new_blobstore_client();
148```
149
150```cpp
151// src/blobstore.cc
152
153#include "cxx-demo/include/blobstore.h"
154
155BlobstoreClient::BlobstoreClient() {}
156
157std::unique_ptr<BlobstoreClient> new_blobstore_client() {
158  return std::unique_ptr<BlobstoreClient>(new BlobstoreClient());
159}
160```
161
162Using `std::make_unique` would work too, as long as you pass `-std=c++14` to the
163C++ compiler as described later on.
164
165The placement in *include/* and *src/* is not significant; you can place C++
166code anywhere else in the crate as long as you use the right paths throughout
167the tutorial.
168
169Be aware that *CXX does not look at any of these files.* You're free to put
170arbitrary C++ code in here, #include your own libraries, etc. All we do is emit
171static assertions against what you provide in the headers.
172
173## Compiling the C++ code with Cargo
174
175Cargo has a [build scripts] feature suitable for compiling non-Rust code.
176
177We need to introduce a new build-time dependency on CXX's C++ code generator in
178Cargo.toml:
179
180```toml,hidelines
181## Cargo.toml
182# [package]
183# name = "cxx-demo"
184# version = "0.1.0"
185# edition = "2018"
186
187[dependencies]
188cxx = "1.0"
189
190[build-dependencies]
191cxx-build = "1.0"
192```
193
194Then add a build.rs build script adjacent to Cargo.toml to run the cxx-build
195code generator and C++ compiler. The relevant arguments are the path to the Rust
196source file containing the cxx::bridge language boundary definition, and the
197paths to any additional C++ source files to be compiled during the Rust crate's
198build.
199
200```rust,noplayground
201// build.rs
202
203fn main() {
204    cxx_build::bridge("src/main.rs")
205        .file("src/blobstore.cc")
206        .compile("cxx-demo");
207}
208```
209
210This build.rs would also be where you set up C++ compiler flags, for example if
211you'd like to have access to `std::make_unique` from C++14. See the page on
212***[Cargo-based builds](build/cargo.md)*** for more details about CXX's Cargo
213integration.
214
215```rust,noplayground
216# // build.rs
217#
218# fn main() {
219    cxx_build::bridge("src/main.rs")
220        .file("src/blobstore.cc")
221        .flag_if_supported("-std=c++14")
222        .compile("cxx-demo");
223# }
224```
225
226[build scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
227
228The project should now build and run successfully, though not do anything useful
229yet.
230
231```console
232cxx-demo$  cargo run
233  Compiling cxx-demo v0.1.0
234  Finished dev [unoptimized + debuginfo] target(s) in 0.34s
235  Running `target/debug/cxx-demo`
236
237cxx-demo$
238```
239
240## Calling a Rust function from C++
241
242Our C++ blobstore supports a `put` operation for a discontiguous buffer upload.
243For example we might be uploading snapshots of a circular buffer which would
244tend to consist of 2 pieces, or fragments of a file spread across memory for
245some other reason (like a rope data structure).
246
247We'll express this by handing off an iterator over contiguous borrowed chunks.
248This loosely resembles the API of the widely used `bytes` crate's `Buf` trait.
249During a `put`, we'll make C++ call back into Rust to obtain contiguous chunks
250of the upload (all with no copying or allocation on the language boundary). In
251reality the C++ client might contain some sophisticated batching of chunks
252and/or parallel uploading that all of this ties into.
253
254```rust,noplayground
255// src/main.rs
256
257#[cxx::bridge]
258mod ffi {
259    extern "Rust" {
260        type MultiBuf;
261
262        fn next_chunk(buf: &mut MultiBuf) -> &[u8];
263    }
264
265    unsafe extern "C++" {
266        include!("cxx-demo/include/blobstore.h");
267
268        type BlobstoreClient;
269
270        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
271        fn put(&self, parts: &mut MultiBuf) -> u64;
272    }
273}
274#
275# fn main() {
276#     let client = ffi::new_blobstore_client();
277# }
278```
279
280Any signature having a `self` parameter (the Rust name for C++'s `this`) is
281considered a method / non-static member function. If there is only one `type` in
282the surrounding extern block, it'll be a method of that type. If there is more
283than one `type`, you can disambiguate which one a method belongs to by writing
284`self: &BlobstoreClient` in the argument list.
285
286As usual, now we need to provide Rust definitions of everything declared by the
287`extern "Rust"` block and a C++ definition of the new signature declared by the
288`extern "C++"` block.
289
290```rust,noplayground
291// src/main.rs
292#
293# #[cxx::bridge]
294# mod ffi {
295#     extern "Rust" {
296#         type MultiBuf;
297#
298#         fn next_chunk(buf: &mut MultiBuf) -> &[u8];
299#     }
300#
301#     unsafe extern "C++" {
302#         include!("cxx-demo/include/blobstore.h");
303#
304#         type BlobstoreClient;
305#
306#         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
307#         fn put(&self, parts: &mut MultiBuf) -> u64;
308#     }
309# }
310
311// An iterator over contiguous chunks of a discontiguous file object. Toy
312// implementation uses a Vec<Vec<u8>> but in reality this might be iterating
313// over some more complex Rust data structure like a rope, or maybe loading
314// chunks lazily from somewhere.
315pub struct MultiBuf {
316    chunks: Vec<Vec<u8>>,
317    pos: usize,
318}
319
320pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
321    let next = buf.chunks.get(buf.pos);
322    buf.pos += 1;
323    next.map_or(&[], Vec::as_slice)
324}
325#
326# fn main() {
327#     let client = ffi::new_blobstore_client();
328# }
329```
330
331```cpp,hidelines
332// include/blobstore.h
333
334# #pragma once
335# #include <memory>
336#
337struct MultiBuf;
338
339class BlobstoreClient {
340public:
341  BlobstoreClient();
342  uint64_t put(MultiBuf &buf) const;
343};
344#
345#std::unique_ptr<BlobstoreClient> new_blobstore_client();
346```
347
348In blobstore.cc we're able to call the Rust `next_chunk` function, exposed to
349C++ by a header `main.rs.h` generated by the CXX code generator. In CXX's Cargo
350integration this generated header has a path containing the crate name, the
351relative path of the Rust source file within the crate, and a `.rs.h` extension.
352
353```cpp,hidelines
354// src/blobstore.cc
355
356##include "cxx-demo/include/blobstore.h"
357##include "cxx-demo/src/main.rs.h"
358##include <functional>
359#
360# BlobstoreClient::BlobstoreClient() {}
361#
362# std::unique_ptr<BlobstoreClient> new_blobstore_client() {
363#   return std::make_unique<BlobstoreClient>();
364# }
365
366// Upload a new blob and return a blobid that serves as a handle to the blob.
367uint64_t BlobstoreClient::put(MultiBuf &buf) const {
368  // Traverse the caller's chunk iterator.
369  std::string contents;
370  while (true) {
371    auto chunk = next_chunk(buf);
372    if (chunk.size() == 0) {
373      break;
374    }
375    contents.append(reinterpret_cast<const char *>(chunk.data()), chunk.size());
376  }
377
378  // Pretend we did something useful to persist the data.
379  auto blobid = std::hash<std::string>{}(contents);
380  return blobid;
381}
382```
383
384This is now ready to use. :)
385
386```rust,noplayground
387// src/main.rs
388#
389# #[cxx::bridge]
390# mod ffi {
391#     extern "Rust" {
392#         type MultiBuf;
393#
394#         fn next_chunk(buf: &mut MultiBuf) -> &[u8];
395#     }
396#
397#     unsafe extern "C++" {
398#         include!("cxx-demo/include/blobstore.h");
399#
400#         type BlobstoreClient;
401#
402#         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
403#         fn put(&self, parts: &mut MultiBuf) -> u64;
404#     }
405# }
406#
407# pub struct MultiBuf {
408#     chunks: Vec<Vec<u8>>,
409#     pos: usize,
410# }
411# pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
412#     let next = buf.chunks.get(buf.pos);
413#     buf.pos += 1;
414#     next.map_or(&[], Vec::as_slice)
415# }
416
417fn main() {
418    let client = ffi::new_blobstore_client();
419
420    // Upload a blob.
421    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
422    let mut buf = MultiBuf { chunks, pos: 0 };
423    let blobid = client.put(&mut buf);
424    println!("blobid = {}", blobid);
425}
426```
427
428```console
429cxx-demo$  cargo run
430  Compiling cxx-demo v0.1.0
431  Finished dev [unoptimized + debuginfo] target(s) in 0.41s
432  Running `target/debug/cxx-demo`
433
434blobid = 9851996977040795552
435```
436
437## Interlude: What gets generated?
438
439For the curious, it's easy to look behind the scenes at what CXX has done to
440make these function calls work. You shouldn't need to do this during normal
441usage of CXX, but for the purpose of this tutorial it can be educative.
442
443CXX comprises *two* code generators: a Rust one (which is the cxx::bridge
444attribute procedural macro) and a C++ one.
445
446### Rust generated code
447
448It's easiest to view the output of the procedural macro by installing
449[cargo-expand]. Then run `cargo expand ::ffi` to macro-expand the `mod ffi`
450module.
451
452[cargo-expand]: https://github.com/dtolnay/cargo-expand
453
454```console
455cxx-demo$  cargo install cargo-expand
456cxx-demo$  cargo expand ::ffi
457```
458
459You'll see some deeply unpleasant code involving `#[repr(C)]`, `#[link_name]`,
460and `#[export_name]`.
461
462### C++ generated code
463
464For debugging convenience, `cxx_build` links all generated C++ code into Cargo's
465target directory under *target/cxxbridge/*.
466
467```console
468cxx-demo$  exa -T target/cxxbridge/
469target/cxxbridge
470├── cxx-demo
471│  └── src
472│     ├── main.rs.cc -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/sources/cxx-demo/src/main.rs.cc
473│     └── main.rs.h -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/include/cxx-demo/src/main.rs.h
474└── rust
475   └── cxx.h -> ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cxx-1.0.0/include/cxx.h
476```
477
478In those files you'll see declarations or templates of any CXX Rust types
479present in your language boundary (like `rust::Slice<T>` for `&[T]`) and `extern
480"C"` signatures corresponding to your extern functions.
481
482If it fits your workflow better, the CXX C++ code generator is also available as
483a standalone executable which outputs generated code to stdout.
484
485```console
486cxx-demo$  cargo install cxxbridge-cmd
487cxx-demo$  cxxbridge src/main.rs
488```
489
490## Shared data structures
491
492So far the calls in both directions above only used **opaque types**, not
493**shared structs**.
494
495Shared structs are data structures whose complete definition is visible to both
496languages, making it possible to pass them by value across the language
497boundary. Shared structs translate to a C++ aggregate-initialization compatible
498struct exactly matching the layout of the Rust one.
499
500As the last step of this demo, we'll use a shared struct `BlobMetadata` to pass
501metadata about blobs between our Rust application and C++ blobstore client.
502
503```rust,noplayground
504// src/main.rs
505
506#[cxx::bridge]
507mod ffi {
508    struct BlobMetadata {
509        size: usize,
510        tags: Vec<String>,
511    }
512
513    extern "Rust" {
514        // ...
515#         type MultiBuf;
516#
517#         fn next_chunk(buf: &mut MultiBuf) -> &[u8];
518    }
519
520    unsafe extern "C++" {
521        // ...
522#         include!("cxx-demo/include/blobstore.h");
523#
524#         type BlobstoreClient;
525#
526#         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
527#         fn put(&self, parts: &mut MultiBuf) -> u64;
528        fn tag(&self, blobid: u64, tag: &str);
529        fn metadata(&self, blobid: u64) -> BlobMetadata;
530    }
531}
532#
533# pub struct MultiBuf {
534#     chunks: Vec<Vec<u8>>,
535#     pos: usize,
536# }
537# pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
538#     let next = buf.chunks.get(buf.pos);
539#     buf.pos += 1;
540#     next.map_or(&[], Vec::as_slice)
541# }
542
543fn main() {
544    let client = ffi::new_blobstore_client();
545
546    // Upload a blob.
547    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
548    let mut buf = MultiBuf { chunks, pos: 0 };
549    let blobid = client.put(&mut buf);
550    println!("blobid = {}", blobid);
551
552    // Add a tag.
553    client.tag(blobid, "rust");
554
555    // Read back the tags.
556    let metadata = client.metadata(blobid);
557    println!("tags = {:?}", metadata.tags);
558}
559```
560
561```cpp,hidelines
562// include/blobstore.h
563#
564# #pragma once
565# #include "rust/cxx.h"
566# #include <memory>
567
568struct MultiBuf;
569struct BlobMetadata;
570
571class BlobstoreClient {
572public:
573  BlobstoreClient();
574  uint64_t put(MultiBuf &buf) const;
575  void tag(uint64_t blobid, rust::Str tag) const;
576  BlobMetadata metadata(uint64_t blobid) const;
577
578private:
579  class impl;
580  std::shared_ptr<impl> impl;
581};
582#
583# std::unique_ptr<BlobstoreClient> new_blobstore_client();
584```
585
586```cpp,hidelines
587// src/blobstore.cc
588
589##include "cxx-demo/include/blobstore.h"
590##include "cxx-demo/src/main.rs.h"
591##include <algorithm>
592##include <functional>
593##include <set>
594##include <string>
595##include <unordered_map>
596
597// Toy implementation of an in-memory blobstore.
598//
599// In reality the implementation of BlobstoreClient could be a large
600// complex C++ library.
601class BlobstoreClient::impl {
602  friend BlobstoreClient;
603  using Blob = struct {
604    std::string data;
605    std::set<std::string> tags;
606  };
607  std::unordered_map<uint64_t, Blob> blobs;
608};
609
610BlobstoreClient::BlobstoreClient() : impl(new class BlobstoreClient::impl) {}
611#
612# // Upload a new blob and return a blobid that serves as a handle to the blob.
613# uint64_t BlobstoreClient::put(MultiBuf &buf) const {
614#   // Traverse the caller's chunk iterator.
615#   std::string contents;
616#   while (true) {
617#     auto chunk = next_chunk(buf);
618#     if (chunk.size() == 0) {
619#       break;
620#     }
621#     contents.append(reinterpret_cast<const char *>(chunk.data()), chunk.size());
622#   }
623#
624#   // Insert into map and provide caller the handle.
625#   auto blobid = std::hash<std::string>{}(contents);
626#   impl->blobs[blobid] = {std::move(contents), {}};
627#   return blobid;
628# }
629
630// Add tag to an existing blob.
631void BlobstoreClient::tag(uint64_t blobid, rust::Str tag) const {
632  impl->blobs[blobid].tags.emplace(tag);
633}
634
635// Retrieve metadata about a blob.
636BlobMetadata BlobstoreClient::metadata(uint64_t blobid) const {
637  BlobMetadata metadata{};
638  auto blob = impl->blobs.find(blobid);
639  if (blob != impl->blobs.end()) {
640    metadata.size = blob->second.data.size();
641    std::for_each(blob->second.tags.cbegin(), blob->second.tags.cend(),
642                  [&](auto &t) { metadata.tags.emplace_back(t); });
643  }
644  return metadata;
645}
646#
647# std::unique_ptr<BlobstoreClient> new_blobstore_client() {
648#   return std::make_unique<BlobstoreClient>();
649# }
650```
651
652```console
653cxx-demo$  cargo run
654  Running `target/debug/cxx-demo`
655
656blobid = 9851996977040795552
657tags = ["rust"]
658```
659
660*You've now seen all the code involved in the tutorial. It's available all
661together in runnable form in the* demo *directory of
662<https://github.com/dtolnay/cxx>. You can run it directly without stepping
663through the steps above by running `cargo run` from that directory.*
664
665<br>
666
667# Takeaways
668
669The key contribution of CXX is it gives you Rust&ndash;C++ interop in which
670*all* of the Rust side of the code you write *really* looks like you are just
671writing normal Rust, and the C++ side *really* looks like you are just writing
672normal C++.
673
674You've seen in this tutorial that none of the code involved feels like C or like
675the usual perilous "FFI glue" prone to leaks or memory safety flaws.
676
677An expressive system of opaque types, shared types, and key standard library
678type bindings enables API design on the language boundary that captures the
679proper ownership and borrowing contracts of the interface.
680
681CXX plays to the strengths of the Rust type system *and* C++ type system *and*
682the programmer's intuitions. An individual working on the C++ side without a
683Rust background, or the Rust side without a C++ background, will be able to
684apply all their usual intuitions and best practices about development in their
685language to maintain a correct FFI.
686
687<br><br>
688