-
Notifications
You must be signed in to change notification settings - Fork 117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Expose enum api #136
WIP: Expose enum api #136
Conversation
a1b9cfa
to
f05d437
Compare
Ok(()) | ||
} | ||
|
||
fn dispatch(&mut self, req: &AnyRequest) -> Result<Response, Errno> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that this returns Result<Response, Error>
now. Being able to return Errno
and use Rust's standard handling of errors is way better. And returning Response
so that it's checked at compile time is a nice improvement too.
I'm less sure about replacing all the Filesystem
methods with this single dispatch
method though. There are two main downsides that I see:
- users are forced to add this really big
match
clause. They can of course delegate to helper methods, but then it's pretty much like the old interface. - adding a new enum value with default handling is complicated. The current
Filesystem
trait has default handling for many methods, since there are a bunch that don't really need to be implemented for basic filesystems. I guess we can export a function likedefault_handler
, but it seems less nice to make users have a default clause in thematch
which calls it.
For the goal of making ABI changes backwards compatible (which I like, btw!), how about changing Filesystem
so that all the methods take a single argument which is the the enum struct used here, and they would all return Result<Response, Errno>
? To preserve backwards compatibility with the existing API, we could introduce it as Filesystem2
to start with, and deprecate Filesystem
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- users are forced to add this really big
match
clause. They can of course delegate to helper methods, but then it's pretty much like the old interface.
The difference with delegation with the enum API is that you can delegate multiple operations at once, rather than having to delegate each operation separately.
For example: I have a filesystem where I want to handle /
differently to by-commit/*
, also differently to by-ref/**
. With the enum I can essentially do:
fn handler(req: AnyRequest) -> Result<Response, Errno> {
let inode_type = req.inode_no() & 0xf000_0000_0000_0000 >> 60;
match inode_type {
0 => handler_root(req),
1 => handler_by_commit(req),
2 => handler_by_ref(req),
...
_ => Err(Errno::ENOENT),
}
}
Similarly if I have some helper (like a middleware) that wants to handle some requests, but not all:
fn my_helper(req: AnyRequest) -> Result<Response, Errno>;
fn handler(req: AnyRequest) -> Result<Response, Errno> {
match {
op::Forget(_) | op::Lookup(_) => return my_helper(),
_ => (),
}
....
}
I don't need to add that code to each of my trait method implementations.
- adding a new enum value with default handling is complicated. The current
Filesystem
trait has default handling for many methods, since there are a bunch that don't really need to be implemented for basic filesystems. I guess we can export a function likedefault_handler
, but it seems less nice to make users have a default clause in thematch
which calls it.
I hadn't considered this. My assumption was that all operations return ENOSYS
by default, so I was thinking that you'd just have a _ => Err(Errno::ENOSYS)
handler at the bottom of the match
, but I like your idea of delegating to a default_handler
instead. The nice thing is that the user can delegate all the operations they choose not to handle in a single line.
For the goal of making ABI changes backwards compatible (which I like, btw!), how about changing
Filesystem
so that all the methods take a single argument which is the the enum struct used here, and they would all returnResult<Response, Errno>
? To preserve backwards compatibility with the existing API, we could introduce it asFilesystem2
to start with, and deprecateFilesystem
.
If we wanted a async or a multithreaded version we'd also need FilesystemAsync
where each method returns Future<...>
and FilesystemMultiThreaded
where each method takes &self
rather than &mut self
. The enum API removes the coupling of how the user code is called from what operations are available, so instead there is no duplication. It also has advantages WRT async methods in traits. We don't need to box and use &dyn for every trait method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wanted a async or a multithreaded version we'd also need FilesystemAsync where each method returns Future<...> and FilesystemMultiThreaded where each method takes &self rather than &mut self. The enum API removes the coupling of how the user code is called from what operations are available, so instead there is no duplication. It also has advantages WRT async methods in traits. We don't need to box and use &dyn for every trait method.
This is definitely a strong argument. It'd be great to have async without duplicating a bunch of code! Are you sure this will work though? These enums all contain borrows, and so I feel like that won't play nicely with async, in that all the lifetimes will need to be 'static
.
p.s.
I'm traveling until next week, so may be a little slow to reply
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this will work though? These enums all contain borrows, and so I feel like that won't play nicely with async, in that all the lifetimes will need to be 'static.
I'm pretty sure. The data will still be owned by the future higher up in the async call stack. The code would look something like:
pub async fn serve_async<Handler>(chan: Channel, handler: Handler) -> io::Result<()>
where
Handler: Fn(AnyRequest<'_>) -> Result<Response, Errno>,
{
loop {
let mut buf = vec![];
chan.read_into(buf).await?;
spawn(handle_one(buf, handler.clone(), chan.sender()))
}
}
async fn handle_one<Handler>(buf: Vec<u8>, handler: Handler, sender: Sender) -> Result<Response, Errno>
where
Handler: Fn(AnyRequest<'_>) -> Result<Response, Errno>,
{
let req = AnyRequest::try_from(buf)?;
let resp = match handler(req).await {
Ok(resp) => resp,
Err(errno) => Response::new_error(errno),
};
sender.send(resp);
}
So the buffer is owned by an async stack frame higher up in the async stack. References to that buffer can be passed to lower stack frames with a non-static lifetime. Of course this is not a stack in the traditional sense. rustc will transform the handle_one
async fn into a self-referential struct that looks like:
struct HandleOneFuture {
buf: Vec<u8>,
req: AnyRequest<'buf>,
}
but we don't have to worry about such details.
I don't see any reason that this shouldn't work, but I don't have code that actually does this yet. Baby-steps and all that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, I think you're right. My grasp of the borrow checker is still a little shaky :) I played around in Rust Playground, and the thing that I thought wouldn't work seems to work fine, so ya let's do!
In the next commit I'll convert it to be implemented in terms of our `ll::Operation` enum. I'm doing this as a seperate commit to highlight the diff.
TODO: Also test trait API
We want u8 data, but with u64 alignment so we can cast the data to our `fuse_abi` structs. This represents such a buffer implemented with only safe code.
Can be used to mount and serve requests for filesystems implemented in terms of the enum API. Note: This is a little different to the `mount` and `mount2` APIs in that we've split out mounting from actually serving. This is neat as it removes the need for a separate `spawn_mount` API. If the user wants to run their filesystem from a different thread they can just call `serve_sync` in that thread. It also provides the flexibility to do other work between `mount` and `serve` - such as readyness notification.
Just wanted to check in on this, since I haven't heard from you in a while. Are you still working on this feature, @wmanley? |
Yes. I just got busy with some other things. I did reach somewhat of an impasse with this work though. There's a lot of API duplication between this API and the existing one. It makes the documentation confusing as there isn't one clear way to do things. To resolve this I think I'll break this into two crates - one with the new enum API and one with the existing one. They can live in the same repo and the |
Cool. Hmm, I'm not very excited about two separate crates for the reasons I mentioned before. Maybe it can be in a separate module? |
There are two advantages I see for there being a separate crate - at least for the time being:
So that's the motivation. I could put the whole of the existing API in a module, but I can't see many advantages to that vs. a separate crate - albeit in the same git repo. |
Hmm, I think I stand by my previous argument that the trust benefits from a single crate outweigh those:
|
Just a heads up, I'm probably going to revert the partial implementation that's merged, in a few weeks, if you're no longer actively working on it. |
I'll rebase this once #146 is merged. |
This allows implementing a FUSE filesystem by providing a
FnMut(req: ll::AnyRequest) -> Result<Response, Errno>
and using enum matching rather than by implementingtrait Filesystem
. This has the advantages that:?
andreturn Err(...)
.INodeNo
and call a method there passing the wholeAnyRequest
object, rather than having to forward every one of theFilesystem
methods individually.Reply
API, so the compiler will tell you when you've got it wrong, rather than finding issues in testing.MkNod(x) => {
is a lot shorter thanfn mknod(&mut self, req: &Request, parent: u64, name: &OsStr, mode: u32, _umask: u32, _rdev: u32, reply: ReplyEntry) {
Operation
structs rather than additional arguments to trait methodsReply
orRequest
. We just need a newserve
function. For example you might have aserve_mt
that takes aFn(req: ll::AnyRequest) -> Result<Response, Errno> + Send + Sync
or aserve_async
that takes aFn(req: ll::AnyRequest) -> Future<Result<Response, Errno>>
. Only the dispatch code needs implementing, rather than the whole API surface ofRequest
andReply
.pre_request(ll::AnyRequest)
method is added to aid porting.Have a look at
examples/simple_enum.rs
to get a feel for what this might look like.TODO:
ll::Response
#131 and rebaseexamples/simple_enum
pre_request
totrait Filesystem
to ease portingMaybe for this PR, maybe for a later one:
serve_mt
- Run filesystem multithreadedserve_async
- Run filesystem async