Not a stupid question! That would be the way to do it in a Rust program with far fewer constraints or interacting tradeoffs.
> why doesn't ArcBorrow just store a reference to the Arc and also a reference to the underlying T
That's two words you're copying around on the stack.
Admittedly, that's a negligible cost. We don't care about that cost. I bet that cost never shows up in profiles. It would be premature to optimize for that cost :)
The real reason is within the "There are other reasons" I mentioned above ;)
These other reasons have to do with RawOffsetArc; it's a long story. The short version is that you may not always have an Arc<T> that you're creating an ArcBorrow from, it may be something else.
So basically this code is Servo's style system, and it is being used by Gecko (Firefox's browser engine). Gecko is in C++.
Servo's style system is quite parallel. So certain things are shared via Arc<T>. Pretty normal.
However, some of these things are shared with C++ code too! We've taught Gecko's refcounting setup about what an Arc is, and it does the appropriate FFI call when it needs to addref/decref it. This is all great. It works. These types are otherwise opaque to Gecko, and it does FFI to get to each.
However, we have one struct, ComputedValues, which stores all the "style structs" (where computed CSS styles go). It's basically a bunch of Arc<T>s of these style structs. ComputedValues is a Rust-side thing, and it's stored in a heap allocation dangling off a "style context" in the C++ code. It's otherwise opaque to C++.
The main operation Gecko does with ComputedValues is fetch a style struct. The style structs are C++ structs which both C++ and Rust can understand. So these getters are a bunch of FFI calls that take ComputedValues. This FFI call turns out to have an overhead that turns up in profiles, and there's an extra cache miss involved in hopping to the ComputedValues allocation (which also turns up in profiles). Both are major.
The fix is to store ComputedValues inline in the style contexts, and make it non-opaque so that C++ can actually read the types. Basically, C++ should see some regular pointers to the style structs. Rust will see Arc<T>.
But Arc<T> is a pointer to the allocation of the Arc. Arc is allocated with the refcount first, and the type T next. And the Rust struct layout isn't something C++ can understand, so code that assumes the offsets and does pointer arithmetic will be brittle and can change in a Rust upgrade. Thus arises RawOffsetArc<T> (https://doc.servo.org/servo_arc/struct.RawOffsetArc.html), which is represented as a pointer to the T, but it has the foreknowledge that T is arc-allocated and has a refcount preceding it. RawOffsetArc<T> is the same as an Arc<T> in all other aspects.
So these structs are now stored in a RawOffsetArc<T>, to make the pointers match with the C++ side representation.
However, there's also pure servo code that uses Arc<T> for this. So we can't just pass around &RawOffsetArc<T> because the servo code doesn't have that. It's not easy to migrate, nor do we really want to (Arc<T> has some more APIs and I don't want to add support for all that to RawOffsetArc). So it becomes easier to create ArcBorrow<T> as something that is guaranteed to have come from either a RawOffsetArc<T> or an Arc<T> (both are the same in behavior and heap representation, just that their stack pointer representation is offset. Converting between the two is a simple pointer bump on the stack). Because they're the same, ArcBorrow<T> can just be a pointer to the T, and the rest works out.
This is one of the reasons -- the other reason is that unlike Rust, where the refcounting is done by the wrapper (you can stick anything in Arc<T> and Arc<T> will handle the refcount), Gecko puts the burden of refcounting on the inner type. This means that if you use RefPtr<Foo> in Gecko, RefPtr will not create a refcount for you, Foo is expected to have AddRef()/Release() methods, which usually bump a refcount field it defines. Furthermore, it's taken as a given that if you have a `Foo`, it is heap allocated (and thus can be refcounted).
This means that having Foo instead of RefPtr<Foo>* is pretty common in Gecko. And it gets passed over FFI a lot to Servo, which again has to either construct transient Arcs, or treat it as an ArcBorrow. We currently do both, but I'm planning on migrating stuff to be more reliant on the ArcBorrow model since it leads to cleaner code.
(A lot of this complexity stems from the fact that browser engines are pretty tightly coupled codebases, and thus the "style system" doesn't have a clean API boundary. There's a lot of reaching into each others' guts that is necessary to make this work)
> why doesn't ArcBorrow just store a reference to the Arc and also a reference to the underlying T
That's two words you're copying around on the stack.
Admittedly, that's a negligible cost. We don't care about that cost. I bet that cost never shows up in profiles. It would be premature to optimize for that cost :)
The real reason is within the "There are other reasons" I mentioned above ;)
These other reasons have to do with RawOffsetArc; it's a long story. The short version is that you may not always have an Arc<T> that you're creating an ArcBorrow from, it may be something else.
So basically this code is Servo's style system, and it is being used by Gecko (Firefox's browser engine). Gecko is in C++.
Servo's style system is quite parallel. So certain things are shared via Arc<T>. Pretty normal.
However, some of these things are shared with C++ code too! We've taught Gecko's refcounting setup about what an Arc is, and it does the appropriate FFI call when it needs to addref/decref it. This is all great. It works. These types are otherwise opaque to Gecko, and it does FFI to get to each.
However, we have one struct, ComputedValues, which stores all the "style structs" (where computed CSS styles go). It's basically a bunch of Arc<T>s of these style structs. ComputedValues is a Rust-side thing, and it's stored in a heap allocation dangling off a "style context" in the C++ code. It's otherwise opaque to C++.
The main operation Gecko does with ComputedValues is fetch a style struct. The style structs are C++ structs which both C++ and Rust can understand. So these getters are a bunch of FFI calls that take ComputedValues. This FFI call turns out to have an overhead that turns up in profiles, and there's an extra cache miss involved in hopping to the ComputedValues allocation (which also turns up in profiles). Both are major.
The fix is to store ComputedValues inline in the style contexts, and make it non-opaque so that C++ can actually read the types. Basically, C++ should see some regular pointers to the style structs. Rust will see Arc<T>.
But Arc<T> is a pointer to the allocation of the Arc. Arc is allocated with the refcount first, and the type T next. And the Rust struct layout isn't something C++ can understand, so code that assumes the offsets and does pointer arithmetic will be brittle and can change in a Rust upgrade. Thus arises RawOffsetArc<T> (https://doc.servo.org/servo_arc/struct.RawOffsetArc.html), which is represented as a pointer to the T, but it has the foreknowledge that T is arc-allocated and has a refcount preceding it. RawOffsetArc<T> is the same as an Arc<T> in all other aspects.
So these structs are now stored in a RawOffsetArc<T>, to make the pointers match with the C++ side representation.
However, there's also pure servo code that uses Arc<T> for this. So we can't just pass around &RawOffsetArc<T> because the servo code doesn't have that. It's not easy to migrate, nor do we really want to (Arc<T> has some more APIs and I don't want to add support for all that to RawOffsetArc). So it becomes easier to create ArcBorrow<T> as something that is guaranteed to have come from either a RawOffsetArc<T> or an Arc<T> (both are the same in behavior and heap representation, just that their stack pointer representation is offset. Converting between the two is a simple pointer bump on the stack). Because they're the same, ArcBorrow<T> can just be a pointer to the T, and the rest works out.
This is one of the reasons -- the other reason is that unlike Rust, where the refcounting is done by the wrapper (you can stick anything in Arc<T> and Arc<T> will handle the refcount), Gecko puts the burden of refcounting on the inner type. This means that if you use RefPtr<Foo> in Gecko, RefPtr will not create a refcount for you, Foo is expected to have AddRef()/Release() methods, which usually bump a refcount field it defines. Furthermore, it's taken as a given that if you have a `Foo`, it is heap allocated (and thus can be refcounted).
This means that having Foo instead of RefPtr<Foo>* is pretty common in Gecko. And it gets passed over FFI a lot to Servo, which again has to either construct transient Arcs, or treat it as an ArcBorrow. We currently do both, but I'm planning on migrating stuff to be more reliant on the ArcBorrow model since it leads to cleaner code.
(A lot of this complexity stems from the fact that browser engines are pretty tightly coupled codebases, and thus the "style system" doesn't have a clean API boundary. There's a lot of reaching into each others' guts that is necessary to make this work)