vs-streamjsonrpc icon indicating copy to clipboard operation
vs-streamjsonrpc copied to clipboard

Example in F#

Open roboz0r opened this issue 2 years ago • 3 comments

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

roboz0r avatar Jan 24 '22 23:01 roboz0r

Thank you for contributing!

AArnott avatar Jan 24 '22 23:01 AArnott

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 :)

baronfel avatar May 31 '22 20:05 baronfel

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?

AArnott avatar Jun 01 '22 14:06 AArnott