For protobuf in C++, we've been moving more and more in the direction of using arenas for memory allocation. When you parse a protobuf, it creates a tree of objects that are usually all deleted at the same time. Freeing an arena is much, much cheaper than traversing the tree of objects and calling free() on each one.
My dream has been that Rust protobuf could support arenas as well as C++, but use Rust's type system to make it all provably correct at compile time (in C++ the lifetime management is inherently manual and unsafe). For absolute top performance, arenas will always beat trees of unique pointers (which I think corresponds to Rust's Box<> type).
I don't know Rust's type/lifetime system well enough to know if this is possible. I was looking recently at arenas in Rust and I noticed that Rust's version of placement new seems to be stalled:
"Unfortunately the path forward for placement new in Rust does not look good right now, so I've reverted this crate to work more like a memory heap where stuff can be put, but not constructed in place."
https://docs.rs/light_arena/1.0.1/light_arena/
Does anyone know more about this?
It's built on top of the Blob traits exposed by pb-jelly. It's not yet open-source, but it would be a good candidate to do next! It also definitely has unsafe code to your point. We open sourced the safe implementations that uses more standard types (Bytes/Buffer/Vec) first.
There's a decent amount of cleanup needed before we can opensource that as well, as much of it was built years ago, when rust ecosystem was less mature (eg Bytes/Buffer weren't around yet).
I like where you're thinking!
Probably the more difficult piece then is just how to model arena ownership of a tree of objects that all have links between them. We want to guarantee that links to sub-messages remain valid, which we would expect to be true if they are all in the same arena. But I believe Rust allows moving/swapping objects in and out of the arena?
To expand a bit on what pcwalton said: Rust never had a concept of a type you can't move (we likely can't ever introduce it) and placement was never about "in-place construction" (Rust doesn't even have a concept of "construction", but rather focuses on initialization).
There is `Pin<&mut T>` which prevents e.g. replacing the value behind it with another `T` one, but there the restriction is in the pointer itself (you can think of it as `&pin mut T`), not in the pointee, and it's not that relevant here, it mainly exists to allow library code to correctly interface with the internally self-referential generators (that `async fn`s are built on).
In the scenario you describe you would use shared references (with appropriate lifetime parameterization on relevant data types), not mutable ones (which imply exclusivity), nor raw pointers (which is the only way to get the C++-like problems). That will limit you to immutable data by default, so if you want to mutate some leaf fields, you wrap them in `Cell` (which prevents invalidation because `&Cell<T>` doesn't allow creating references inside the `T` value) or some kind of atomic/lock if you want concurrent access.
This already solves all the problems you were thinking of, and it doesn't even fundamentally require an arena (what you need an arena for is the dynamic graph structure, assuming you want to use references/pointers and not integer indices).
In essence, the more you rely on safe Rust features, the harder this is to get wrong or end up with a misusable API.
Oh and the Rust compiler itself has used arenas since before 1.0 (IIRC), and with every year we move more data into them, though most are 100% immutable (which helps with compiler correctness, especially incremental recompilation), or even interned (deep deduplication by caching allocation attempts). We can even get away without dynamically tracking destructors by making most arena-allocated data destructor-less (we mostly just had to replace `Vec<T>` with arena-allocated `&[T]` to make that work).
Without placement new allocating an object involves:
a) Constructing the object on the stack
b) Copying it to the heap
With placement new you construct the object directly on the heap, thus avoiding a copy.
It's purely an optimization to avoid a single copy. It's a bit surprising that Rust doesn't have it, given the focus on control over such things, and that it had an experimental 'box' keyword for this at 1.0.
edit: To be clear, the extra copy is not guaranteed - compilers can elide it. In order to help the compiler do this you can look at crates like boxext, which provide extension methods on Box that help the compiler to remove the copy. But placement new guarantees this.
Edit: GATs are how "placement new" (Rust doesn't have new at all, hence the airquotes) would work in Rust, assuming the author here meant to say "custom allocators." With GATs, you could create a pointer (Box, Arc, ArenaBox etc.) trait and use that on your message types.[3] "Placement new" is a whole different issue that ultimately boils down to a compiler optimization that is currently missing/not working and shouldn't functionally affect what you are trying to do at all.
[1]: https://github.com/fitzgen/bumpalo [2]: https://github.com/fitzgen/dodrio [3]: https://rust-lang.github.io/rfcs/1598-generic_associated_typ...
Some more context. There used to be a `box` keyword too. https://github.com/rust-lang/rust/issues/50047
> The core of this crate is a python2 script codegen.py that is provided to the protobuf compiler, protoc as a plugin.
That's... surprisingly janky. Not only Python tooling is always painful to deal with (compared to Go/Rust/...), but Python 2? And in a project that otherwise has no reason to depend on Python? :(
In comparison, the Go protoc plugin is written in Go, the alternatice rust-protobuf protoc plugin is witten in Rust, the Typescript one is written in Typescript...
See issues https://github.com/dropbox/pb-jelly/issues/37 and https://github.com/dropbox/pb-jelly/issues/40 for context.
My $dayjob also started as a large python code base, and so there are still lots of one-off Py2 scripts that need to be deleted or rewritten.
So they're maintaining a project with two languages with Python 2 as a hard requirement for code generation? Oh dear.
This tells me that this project will have the same fate as djinni [0] or their similar archived projects.
protoc plugins have an interesting bootstrapping problem as well: the protoc-gen-$LANG interface requires the ability to ser/de protobuf messages that describe the proto file's AST. If your build system builds almost everything from scratch, including the protoc plugin, this means that you need to have a variant of your protoc plugin linked to a working proto implementation...
That's not to say this is impossible or even difficult, but at the time that I last looked at it (more than a year ago at this point), it made it fairly unpalatable to move the codegen from Python to Rust.
I've chosen Prost for our project, but see the whole list:
https://github.com/stepancheg/rust-protobuf#related-projects
But yeah - this is one of those things that makes me stay with Go instead of moving over to Rust for my backend SOA/microservice work. In Rust, for everything you need to do, there's at least 5 different libraries that implement that, all competing with eachother. This is especially annoying when dealing with transitive dependencies. Meanwhile in Go, you generally get one choice - it might be not great, but that's fine, it doesn't have to be.
EDIT: This is not intended to be mindless bashing of Rust. I do use Rust for other things. It's a fine language.
That's interesting. This seems like a good thing to me. I've had no issues with microservices in Rust related to having too many crates.
And, it's not even protobuf I have a problem with - but things like HTTP implementations. There still isn't a canonical HTTP client/server implementation for Rust, while in Go basically everyone just uses `net/http`, or something that builds on top of that. Same for cryptographic primitives, TLS, context, ...
[1] - https://docs.google.com/document/d/19kfhro7-CnBdFqFk7l4_Hmwa...
Rust has such great support for performant zero copy serialization and de-serialization in various formats (bincode, message pack, cbor, bson). Seeing this for protobuf feels very encouraging.
There are definitely some downsides to the approach though, mostly typical problems you would expect with machine generated code. Namely that it’s verbose and if you have a super complex protobuf data model (hundreds or thousands of fields) and want to ship a fatjar or similar bundling of dependencies you can run into some size issues.
Plus, Any [1] is an effort to standardize the ability to carry the proto type (as a global identifier that can be used to retrieve its schema) alongside its serialized format.
[1] - https://developers.google.com/protocol-buffers/docs/proto3#a...
It's a piece of software, it has a design that is either fit for purpose or not. When ego becomes entangled in that design process, it's a strong indicator of the kind of experience one might have trying to get fixes or enhancements merged, or even the kind of attitude you'd find when attempting to report a bug.