Write your own struct that contains the Arc<RwLock<Foo>>.
#[derive(Clone, Debug)]
pub struct FooReadOnly(Arc<RwLock<Foo>>);
impl FooReadOnly {
pub fn read(&self) -> LockResult<RwLockReadGuard<'_, Foo>> {
self.0.read()
}
}
(A fully fleshed-out version would also contain a wrapper for try_read().)
The general pattern worth noting here is: instead of your data type being visibly inside an Arc<RwLock<...>>, your public type contains the Arc. This allows much more flexibility in what kind of “handles” you can offer than exposing the Arc. This is a fairly common pattern in Rust — if you've ever used a library type that notes you can clone it and get another handle to the same thing, there's a good chance it's doing the same thing inside (if it's not actually a handle to an OS resource like a file descriptor).
If you wanted to fully hide the implementation details, you would also wrap the RwLockReadGuard — all it needs to do is implement Deref<Target = Foo> and forward that to the guard. Such wrappers can also do things like Derefing to some part of the Foo rather than an &Foo exactly.