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

Example in F# #759

Open
roboz0r opened this issue Jan 24, 2022 · 3 comments
Open

Example in F# #759

roboz0r opened this issue Jan 24, 2022 · 3 comments
Labels

Comments

@roboz0r
Copy link

roboz0r commented Jan 24, 2022

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
@AArnott
Copy link
Member

AArnott commented Jan 24, 2022

Thank you for contributing!

@AArnott AArnott added the docs label Jan 24, 2022
@baronfel
Copy link
Member

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

@AArnott
Copy link
Member

AArnott commented Jun 1, 2022

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants