Skip to content
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

Add support for rpcrequest from vim script #4

Merged

Conversation

boxofrox
Copy link
Contributor

@boxofrox boxofrox commented Mar 23, 2017

Make the write socket available to the dispatch thread so an rpc
response may be sent back to the requesting vim script.

Rename cb to notify_handler.

Add request_handler to start_event_loop_cb. Similar to
notify_handler, but returns a result to be sent in the rpc response.

Flush buffered write sockets, otherwise vim will wait indefinitely for
the rpc response.

Not certain whether this is a direction you wanted to go. If not, let me know what you had in mind and I'll try to rewrite this PR to accommodate.

@boxofrox
Copy link
Contributor Author

boxofrox commented Mar 23, 2017

This patch lays the foundation for the next PR: using jobstart in vimscript to execute a rust plugin and establish an RPC session over that process's stdin/stdout. feature/session-from-neovim-parent-process

@boxofrox boxofrox force-pushed the feature/support-rpc-request-from-vimscript branch from 7febb91 to 2196e7d Compare March 23, 2017 02:09
@daa84
Copy link
Owner

daa84 commented Mar 23, 2017

Hello, thanks for patch. Looks good. I think maybe better to use Option here for functions? Idea is: if i don't need one of this two callbacks, i don't need to write any addition code like empty callback.

@boxofrox
Copy link
Contributor Author

Sounds good. I'll make it happen.

@boxofrox boxofrox force-pushed the feature/support-rpc-request-from-vimscript branch from 2196e7d to 0c307dd Compare March 23, 2017 16:01
@boxofrox
Copy link
Contributor Author

boxofrox commented Mar 23, 2017

I'm trying to create a plugin where vimscript sends requests to the rust plugin, and the rust plugin responds to requests. I want vimscript to deliver a "quit" command over RPC to kill the rust plugin (among other commands). "quit" is arbitrary and demonstrates a situation where state is shared between the callback and main thread.

To facilitate the "quit" command, I have a is_done : Arc<Mutex<bool>> = false shared with the notify_handler (cannot rely on free function here), so it can flip the boolean when vimscript sends the "quit" event, which subsequently causes the main thread in Rust (which monitors is_done) to clean up and die, terminating the plugin.

The problem I'm having is organizing this [complex?] handler in a closure in such a way that my code remains legible, and I'm not bashing myself against "Expected Type<X>, found Type<closure>" compiler errors in Rust when I refactor my plugin.

It kind of sucks that closures require I define them at the time they're passed as arguments. E.g.

    session.start_event_loop_cb(
        Some(|name, args| {
            // lots of code here
        }),
        Some(|name, args| {
            // lots of code here
            Ok(Value::String("ok".to_owned()))
        }));

A Trait for the callbacks might alleviate my issue, but that adds more Type complexity due to sharing across threads that affects everyone. I imagine some users of neovim-lib would get by with simple closures, so I'm not sure a Trait is the right approach.

pub trait NeovimHandler {
    fn handle_notify (&mut self, name: &str, args: Vec<Value>) {}
    fn handle_request (&mut self, name: &str, args: Vec<Values>) -> Result<Value, Value> {
        Err(Value::String("Not implemented".to_owned()))
    }
}

The only other option I know of are trait objects Box<closure>, and I understand their limitations less right now.

Thoughts?

@boxofrox
Copy link
Contributor Author

I created a version of this patch with a Handler trait for reference https://github.com/boxofrox/neovim-lib/commits/handler-trait.

I think this might work out well with a bit more... work. To mitigate complications with shared state, custom Handler objects will be owned by dispatch_thread. Shared state can be wrapped up in Arc<Mutex<>> as a field in the custom Handler object, and the user defined functions can manage the mutex locks.

For example:

// imagine a complex object with lots fields to share
struct SharedState {
    // insert much shared state here
}

struct MyHandler {
    shared: Arc<Mutex<SharedState>>
}

impl Handler for MyHandler {
    fn handle_notify (&mut self, name: &str, args: Vec<Value>) {
        let shared = self.shared.lock().unwrap();
        // do stuff with `shared`
    }
}

Anyone not working with shared state isn't forced to mess with Arc<Mutex<>>'s.

I'd also like to keep support for closures by added a ClosureHandler type that takes the closures from start_event_loop_cb and encapsulates them. The Handler trait would have it's own start_event_loop_handler function in session.rs.

Sorry for writing so much. I've spent quite some time with this, and it's hard for me to tackle various solutions without losing track of the problems I've had.

@daa84
Copy link
Owner

daa84 commented Mar 24, 2017

I think about it a bit and Optional is bad solution for generics as None does not provide type information. So solution with trait maybe ok.
Another way is to create number of functions with two or one arguments. I think a bit more about this...

@daa84
Copy link
Owner

daa84 commented Mar 24, 2017

Ok i think trait with default implementations of handler's ok for given situation.

To provide support for closures maybe better to use builder pattern for ClosureHandler without generics pointer, but using dynamic dispatch?

ClosureHandler::new().notify_handler(|| {
  // something here
});

Or maybe ClosureHandler just not needed :) as implementation of trait is not a big problem, but gives static dispatch as a result.

@boxofrox
Copy link
Contributor Author

If you're happy dropping the closure callbacks, I'm happy. Since start_event_loop is a one-time call, we don't gain much from closures, and it's trivial to create a Handler implementation, and that only needs be done once, if at all.

I also don't mind keeping support for closures, if desired. It's an interesting exercise to keep the API backward compatible. I'm not sure if the builder pattern gains anything, though maybe it'll drop the match Option statement from the Handler trait functions.

I'll update the PR to use the Handler trait without closures for now.

@boxofrox boxofrox force-pushed the feature/support-rpc-request-from-vimscript branch from 0c307dd to 5c70ee9 Compare March 24, 2017 18:34
pub fn start_event_loop_cb<F: FnMut(&str, Vec<Value>) + Send + 'static>(&mut self, cb: F) {
self.dispatch_guard =
Some(Self::dispatch_thread(self.queue.clone(), self.reader.take().unwrap(), cb));
pub fn start_event_loop_handler<H>(&mut self, handler: Option<H>)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daa84 did you want to drop the Option<H> here and use H instead? In order to do so, DefaultHandler must be exposed to the user, so they can pass along that value when they have no need of callbacks.

@daa84
Copy link
Owner

daa84 commented Mar 25, 2017

My first idea with Option was bad, because Option::None for generics does not work good. So i prefer to use two functions with and without handler as was before but with trait as a handler.

@boxofrox boxofrox force-pushed the feature/support-rpc-request-from-vimscript branch from 5c70ee9 to 90dd0cc Compare March 26, 2017 03:13
@boxofrox
Copy link
Contributor Author

Ok, I think I got it this time. Option is now out.

@daa84
Copy link
Owner

daa84 commented Mar 26, 2017

One last change, can you move out wr.flush() from write_value to encode function? I have plans to move back to rmp value again and think to remove this code :) this is because previously i think Value is removed from rmp value permanently but they are moved this code to separate crate

@boxofrox
Copy link
Contributor Author

Certainly. I'll knock that out sometime Monday.

Neovim will wait indefinitely for an RPC response that is stuck in the
IO buffer of a separate process.

Signed-off-by: Justin Charette <charetjc@gmail.com>
Access to the write socket is required for the dispatch thread to send
a RPC response back to the requesting vim script.

Signed-off-by: Justin Charette <charetjc@gmail.com>
Signed-off-by: Justin Charette <charetjc@gmail.com>
The Handler trait enables users to define handler functions elsewhere,
rather than at the time `session.start_event_loop_cb()` is invoked.

Moving closures elsewhere causes type mismatch errors in the Rust
compiler when `session.start_event_loop_cb()` is invoked.

Signed-off-by: Justin Charette <charetjc@gmail.com>
Signed-off-by: Justin Charette <charetjc@gmail.com>
@boxofrox boxofrox force-pushed the feature/support-rpc-request-from-vimscript branch from 90dd0cc to 6d913af Compare March 27, 2017 15:41
@boxofrox
Copy link
Contributor Author

I moved flush() to the model.encode function. Let me know if it's the wrong spot.

@boxofrox
Copy link
Contributor Author

boxofrox commented Mar 27, 2017

One more thing to consider. A user has to define their handler before they can start the session's event loop, and only then can they create the Neovim object.

If a handler needs to ask Neovim for more information, how might that be done? The Neovim object owns the Session object, which owns the Handler object. A circular dependency forms when the Handler needs to communicate with Neovim before completing its task.

As it stands, it seems the Handler is limited to waiting for Neovim to trigger it. All the Handler processing is done exclusively in Rust, then a response is returned. It's quite a limitation, and I'm wondering if a solution may require more changes to this PR.

One solution might be to use channel so the handler can communicate with the Rust scope holding onto the Neovim object, and that scope will process requests (e.g. getpos) and send the results back to the handler so it can finish it's response to Neovim.

Not sure what to do here, or if we can worry about this later, but I just ran into this issue with my test plugin. I can put this in the issue tracker if you prefer.

@boxofrox
Copy link
Contributor Author

It just occurs to me that a single dispatch thread won't be able to process responses from Neovim while that same thread is executing a Handler method.

@daa84
Copy link
Owner

daa84 commented Mar 28, 2017

Yes, your right, the idea is: special thread waits for response from neovim and if this thread blocks - response can't be taken. So to process make neovim call it is need to make response to be processed. Currently only sync call implemented, so it's not possible to make call and then go to next instruction.

Solution is: take event in event process thread -> send this event (also with id) to channel (don't wait here for response, so thread can continue execution) -> do some process in main thread -> send response with given id in main thread. This way it will work i think :)

@daa84
Copy link
Owner

daa84 commented Mar 28, 2017

I don't think previously of such use case :(

@boxofrox
Copy link
Contributor Author

No worries. 😄

I tried spawning threads to handle events in dispatch_event, but I found Neovim replies to subsequent commands with a WouldBlock error. I'm not sure if this is because my plugin is trying to communicate with Neovim while Neovim is in insert mode, or something else.

If I figure something out that patches neovim-lib, I'll offer up another PR for consideration.

Cheers

@boxofrox
Copy link
Contributor Author

Sending events over channels is working very well. Thanks for the suggestion.

@daa84 daa84 merged commit 28e33bb into daa84:master Mar 29, 2017
@daa84
Copy link
Owner

daa84 commented Mar 29, 2017

Thanks

@boxofrox boxofrox deleted the feature/support-rpc-request-from-vimscript branch March 29, 2017 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants