vs-streamjsonrpc
vs-streamjsonrpc copied to clipboard
Example in F#
After much fiddling I finally have a working proof of concept in F#. I wanted to share the code here to save the next person the headache. It would be good if this could be incorporated into the docs.
// Shared.fs
// Kept in a separate project referenced by both the client and the server
namespace ExampleRPC
open System
open System.IO.Pipes
module Transport =
let pipeName = "SamplePipeName"
let serverPipe () =
new NamedPipeServerStream(
pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous)
let clientPipe () =
new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous)
type IServer =
abstract Message: string -> unit
// Client.fs
module RPCClient
open System
open StreamJsonRpc
open ExampleRPC
/// Returns a proxy interface to the server once the connection has been established.
/// 'T should be the interface type that specifies the server data contract.
let getClientProxy<'T when 'T: not struct> () =
let formatter =
let options =
MessagePackFormatter.DefaultUserDataSerializationOptions
let formatter = new MessagePackFormatter()
formatter.SetMessagePackSerializerOptions options
formatter
let pipe = Transport.clientPipe()
let handler =
new LengthHeaderMessageHandler(pipe, pipe, formatter)
async {
// Client pipe must be connected before returning interface
do! pipe.ConnectAsync() |> Async.AwaitTask
return JsonRpc.Attach<'T>(handler)
}
// Server.fs
module RPCServer
open System
open StreamJsonRpc
open ExampleRPC
type Server() =
interface IServer with
member __.Message(msg:string) =
printfn "%s" msg
/// Creates a background thread to handle incoming connections to the server.
/// 'T should be the interface type that specifies the server data contract.
let createRPCServer<'T when 'T: not struct> (server:'T) =
let formatter =
let options =
MessagePackFormatter.DefaultUserDataSerializationOptions
let formatter = new MessagePackFormatter()
formatter.SetMessagePackSerializerOptions options
formatter
let rec loop () = async {
// A new pipe must be created for each request
let pipe = Transport.serverPipe()
do! pipe.WaitForConnectionAsync() |> Async.AwaitTask
let handler = new LengthHeaderMessageHandler(pipe, pipe, formatter)
use rpc = new JsonRpc(handler)
rpc.AddLocalRpcTarget<'T>(server, JsonRpcTargetOptions())
rpc.StartListening()
// No need to await completion, just loop and prepare new pipe for next request
let _ = rpc.Completion
return! loop()
}
loop ()
|> Async.Start
Thank you for contributing!
For what it's worth, over at Ionide.LanguageServerProtocol (the library that powers FsAutoComplete and csharp-language-server), we use StreamJsonRpc. It's a bit fiddly usage, because we grafted it on over our bespoke implementation, but over time we're standarding on it more.
My only real complaint from this whole conversion is that I'd really love to have some more control over the 'binding' of members to LSP methods. F# Async's have a really nice automatic cancellation and cold-start/composability semantic that makes them fairly natural to use, but unfortunately this library is fairly locked in to Tasks and didn't support plugging in different discoverability mechanisms that I saw (maybe that's not the case?) so we do our own shim mapping by hand. Any insights there would be appreciated :)
Thanks for your feedback, @baronfel. I'm not very familiar with F#, so I'd love to learn more about how we could make StreamJsonRpc be a better fit for you. As it sounds like you've discovered, if the reflection over your RPC target type doesn't suit you, you can add each target method by hand. By a pluggable discovery mechanism, are you thinking of a way to pass your F# type that serves as an RPC target into the library and have it reflect over it looking for other patterns for which methods to assign as RPC targets? Or is there more involved around how to invoke and await them?