• 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##include <string>
360#
361# BlobstoreClient::BlobstoreClient() {}
362#
363# std::unique_ptr<BlobstoreClient> new_blobstore_client() {
364#   return std::make_unique<BlobstoreClient>();
365# }
366
367// Upload a new blob and return a blobid that serves as a handle to the blob.
368uint64_t BlobstoreClient::put(MultiBuf &buf) const {
369  // Traverse the caller's chunk iterator.
370  std::string contents;
371  while (true) {
372    auto chunk = next_chunk(buf);
373    if (chunk.size() == 0) {
374      break;
375    }
376    contents.append(reinterpret_cast<const char *>(chunk.data()), chunk.size());
377  }
378
379  // Pretend we did something useful to persist the data.
380  auto blobid = std::hash<std::string>{}(contents);
381  return blobid;
382}
383```
384
385This is now ready to use. :)
386
387```rust,noplayground
388// src/main.rs
389#
390# #[cxx::bridge]
391# mod ffi {
392#     extern "Rust" {
393#         type MultiBuf;
394#
395#         fn next_chunk(buf: &mut MultiBuf) -> &[u8];
396#     }
397#
398#     unsafe extern "C++" {
399#         include!("cxx-demo/include/blobstore.h");
400#
401#         type BlobstoreClient;
402#
403#         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
404#         fn put(&self, parts: &mut MultiBuf) -> u64;
405#     }
406# }
407#
408# pub struct MultiBuf {
409#     chunks: Vec<Vec<u8>>,
410#     pos: usize,
411# }
412# pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
413#     let next = buf.chunks.get(buf.pos);
414#     buf.pos += 1;
415#     next.map_or(&[], Vec::as_slice)
416# }
417
418fn main() {
419    let client = ffi::new_blobstore_client();
420
421    // Upload a blob.
422    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
423    let mut buf = MultiBuf { chunks, pos: 0 };
424    let blobid = client.put(&mut buf);
425    println!("blobid = {}", blobid);
426}
427```
428
429```console
430cxx-demo$  cargo run
431  Compiling cxx-demo v0.1.0
432  Finished dev [unoptimized + debuginfo] target(s) in 0.41s
433  Running `target/debug/cxx-demo`
434
435blobid = 9851996977040795552
436```
437
438## Interlude: What gets generated?
439
440For the curious, it's easy to look behind the scenes at what CXX has done to
441make these function calls work. You shouldn't need to do this during normal
442usage of CXX, but for the purpose of this tutorial it can be educative.
443
444CXX comprises *two* code generators: a Rust one (which is the cxx::bridge
445attribute procedural macro) and a C++ one.
446
447### Rust generated code
448
449It's easiest to view the output of the procedural macro by installing
450[cargo-expand]. Then run `cargo expand ::ffi` to macro-expand the `mod ffi`
451module.
452
453[cargo-expand]: https://github.com/dtolnay/cargo-expand
454
455```console
456cxx-demo$  cargo install cargo-expand
457cxx-demo$  cargo expand ::ffi
458```
459
460You'll see some deeply unpleasant code involving `#[repr(C)]`, `#[link_name]`,
461and `#[export_name]`.
462
463### C++ generated code
464
465For debugging convenience, `cxx_build` links all generated C++ code into Cargo's
466target directory under *target/cxxbridge/*.
467
468```console
469cxx-demo$  exa -T target/cxxbridge/
470target/cxxbridge
471├── cxx-demo
472│  └── src
473│     ├── main.rs.cc -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/sources/cxx-demo/src/main.rs.cc
474│     └── main.rs.h -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/include/cxx-demo/src/main.rs.h
475└── rust
476   └── cxx.h -> ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cxx-1.0.0/include/cxx.h
477```
478
479In those files you'll see declarations or templates of any CXX Rust types
480present in your language boundary (like `rust::Slice<T>` for `&[T]`) and `extern
481"C"` signatures corresponding to your extern functions.
482
483If it fits your workflow better, the CXX C++ code generator is also available as
484a standalone executable which outputs generated code to stdout.
485
486```console
487cxx-demo$  cargo install cxxbridge-cmd
488cxx-demo$  cxxbridge src/main.rs
489```
490
491## Shared data structures
492
493So far the calls in both directions above only used **opaque types**, not
494**shared structs**.
495
496Shared structs are data structures whose complete definition is visible to both
497languages, making it possible to pass them by value across the language
498boundary. Shared structs translate to a C++ aggregate-initialization compatible
499struct exactly matching the layout of the Rust one.
500
501As the last step of this demo, we'll use a shared struct `BlobMetadata` to pass
502metadata about blobs between our Rust application and C++ blobstore client.
503
504```rust,noplayground
505// src/main.rs
506
507#[cxx::bridge]
508mod ffi {
509    struct BlobMetadata {
510        size: usize,
511        tags: Vec<String>,
512    }
513
514    extern "Rust" {
515        // ...
516#         type MultiBuf;
517#
518#         fn next_chunk(buf: &mut MultiBuf) -> &[u8];
519    }
520
521    unsafe extern "C++" {
522        // ...
523#         include!("cxx-demo/include/blobstore.h");
524#
525#         type BlobstoreClient;
526#
527#         fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
528#         fn put(&self, parts: &mut MultiBuf) -> u64;
529        fn tag(&self, blobid: u64, tag: &str);
530        fn metadata(&self, blobid: u64) -> BlobMetadata;
531    }
532}
533#
534# pub struct MultiBuf {
535#     chunks: Vec<Vec<u8>>,
536#     pos: usize,
537# }
538# pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
539#     let next = buf.chunks.get(buf.pos);
540#     buf.pos += 1;
541#     next.map_or(&[], Vec::as_slice)
542# }
543
544fn main() {
545    let client = ffi::new_blobstore_client();
546
547    // Upload a blob.
548    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
549    let mut buf = MultiBuf { chunks, pos: 0 };
550    let blobid = client.put(&mut buf);
551    println!("blobid = {}", blobid);
552
553    // Add a tag.
554    client.tag(blobid, "rust");
555
556    // Read back the tags.
557    let metadata = client.metadata(blobid);
558    println!("tags = {:?}", metadata.tags);
559}
560```
561
562```cpp,hidelines
563// include/blobstore.h
564
565##pragma once
566##include "rust/cxx.h"
567# #include <memory>
568
569struct MultiBuf;
570struct BlobMetadata;
571
572class BlobstoreClient {
573public:
574  BlobstoreClient();
575  uint64_t put(MultiBuf &buf) const;
576  void tag(uint64_t blobid, rust::Str tag) const;
577  BlobMetadata metadata(uint64_t blobid) const;
578
579private:
580  class impl;
581  std::shared_ptr<impl> impl;
582};
583#
584# std::unique_ptr<BlobstoreClient> new_blobstore_client();
585```
586
587```cpp,hidelines
588// src/blobstore.cc
589
590##include "cxx-demo/include/blobstore.h"
591##include "cxx-demo/src/main.rs.h"
592##include <algorithm>
593##include <functional>
594##include <set>
595##include <string>
596##include <unordered_map>
597
598// Toy implementation of an in-memory blobstore.
599//
600// In reality the implementation of BlobstoreClient could be a large
601// complex C++ library.
602class BlobstoreClient::impl {
603  friend BlobstoreClient;
604  using Blob = struct {
605    std::string data;
606    std::set<std::string> tags;
607  };
608  std::unordered_map<uint64_t, Blob> blobs;
609};
610
611BlobstoreClient::BlobstoreClient() : impl(new class BlobstoreClient::impl) {}
612#
613# // Upload a new blob and return a blobid that serves as a handle to the blob.
614# uint64_t BlobstoreClient::put(MultiBuf &buf) const {
615#   // Traverse the caller's chunk iterator.
616#   std::string contents;
617#   while (true) {
618#     auto chunk = next_chunk(buf);
619#     if (chunk.size() == 0) {
620#       break;
621#     }
622#     contents.append(reinterpret_cast<const char *>(chunk.data()), chunk.size());
623#   }
624#
625#   // Insert into map and provide caller the handle.
626#   auto blobid = std::hash<std::string>{}(contents);
627#   impl->blobs[blobid] = {std::move(contents), {}};
628#   return blobid;
629# }
630
631// Add tag to an existing blob.
632void BlobstoreClient::tag(uint64_t blobid, rust::Str tag) const {
633  impl->blobs[blobid].tags.emplace(tag);
634}
635
636// Retrieve metadata about a blob.
637BlobMetadata BlobstoreClient::metadata(uint64_t blobid) const {
638  BlobMetadata metadata{};
639  auto blob = impl->blobs.find(blobid);
640  if (blob != impl->blobs.end()) {
641    metadata.size = blob->second.data.size();
642    std::for_each(blob->second.tags.cbegin(), blob->second.tags.cend(),
643                  [&](auto &t) { metadata.tags.emplace_back(t); });
644  }
645  return metadata;
646}
647#
648# std::unique_ptr<BlobstoreClient> new_blobstore_client() {
649#   return std::make_unique<BlobstoreClient>();
650# }
651```
652
653```console
654cxx-demo$  cargo run
655  Running `target/debug/cxx-demo`
656
657blobid = 9851996977040795552
658tags = ["rust"]
659```
660
661*You've now seen all the code involved in the tutorial. It's available all
662together in runnable form in the* demo *directory of
663<https://github.com/dtolnay/cxx>. You can run it directly without stepping
664through the steps above by running `cargo run` from that directory.*
665
666<br>
667
668# Takeaways
669
670The key contribution of CXX is it gives you Rust&ndash;C++ interop in which
671*all* of the Rust side of the code you write *really* looks like you are just
672writing normal Rust, and the C++ side *really* looks like you are just writing
673normal C++.
674
675You've seen in this tutorial that none of the code involved feels like C or like
676the usual perilous "FFI glue" prone to leaks or memory safety flaws.
677
678An expressive system of opaque types, shared types, and key standard library
679type bindings enables API design on the language boundary that captures the
680proper ownership and borrowing contracts of the interface.
681
682CXX plays to the strengths of the Rust type system *and* C++ type system *and*
683the programmer's intuitions. An individual working on the C++ side without a
684Rust background, or the Rust side without a C++ background, will be able to
685apply all their usual intuitions and best practices about development in their
686language to maintain a correct FFI.
687
688<br><br>
689