F#でIPCをやってみる

2017/06/26

F#でプロセス間通信を行うことを考えてみる。

無論、丸裸のTCPやUDP、共有メモリやパイプ・メールスロットなんかでもいいわけだが、それだと正直に言って実装がしんどい。だから、何かいい感じの方法は無いものかと探っていたら、.NET Remotingなる方法があるらしいので、試してみた。

■.NET Remoting版

-------------------------------------------------------------------------------------------

open System
open System.Runtime.Remoting
open System.Runtime.Remoting.Channels
open System.Runtime.Remoting.Channels.Ipc
open System.Diagnostics
open System.Threading

// サーバからクライアントに対して公開されるオブジェクト
type CSharedObject() =
    inherit MarshalByRefObject()

    // publicかつ可変なメンバ変数
    [<DefaultValue>]
    val mutable public v2 : int
    let mutable value = 0

    // publicなメソッド
    member this.foo arg =
        let pid = Process.GetCurrentProcess().Id
        let thid = Thread.CurrentThread.ManagedThreadId
        printf "CSharedObject.foo, pid=%d, thid=%d, value=%d, arg=%d\n" pid thid value arg
        value <- arg
        arg + 1         // 意味はないが、+1した値を返してみる

// サーバ側の処理
let Server () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Server Process started. pid = %d, thid=%d\n" pid thid

    // CSharedObjectのオブジェクトを作って公開する
    ChannelServices.RegisterChannel( new IpcServerChannel "test1", true )
    let rSharedObject = new CSharedObject()
    RemotingServices.Marshal( rSharedObject, "test2", typeof< CSharedObject > ) |> ignore

    //************************************************************
    for i = 1 to 60 do
        // サーバ側でv2の値を定期的に更新してみる
        rSharedObject.v2 <- i
        printf "Server, pid=%d, thid=%d, v2=%d\n" pid thid rSharedObject.v2
        System.Threading.Thread.Sleep 1000
    //************************************************************

// クライアント側の処理
let Client () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Client Process started. pid = %d, thid=%d\n" pid thid

    // サーバ側で公開されているオブジェクトを取得する
    ChannelServices.RegisterChannel( new IpcClientChannel(), true )
    let rSharedObject = Activator.GetObject( typeof< CSharedObject >, "ipc://test1/test2" ) :?> CSharedObject

    //************************************************************
    for i = 1 to 10 do
        // 定期的に公開オブジェクトのメソッドを呼ぶ
        printf
            "Client, pid=%d, thid=%d, foo=%d, v2 = %d\n"
            pid thid ( rSharedObject.foo ( i * 100 ) ) rSharedObject.v2
        Threading.Thread.Sleep 500
    //************************************************************

[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-------------------------------------------------------------------------------------------

上記と合わせて、System.Runtime.Remotingを参照設定に追加して、コンパイルしてやったら動いた。

検索して出てきたC#のソースをほぼそのままF#に変えてやっただけではあるが、非常に素直に動作してくれる。

どうも、挙動としてはサーバ側で公開したオブジェクト(上記ではCSharedObjectクラスのインスタンス)のメソッドをクライアント側から呼び出すと、引数がクライアントからサーバに渡されて、サーバ側のプロセス空間で手続きが実行され、戻り値がクライアント側に返されるらしい。

併せて、CSharedObjectクラスのメンバ変数(上記だとCSharedObject.v2)をサーバないしクライアントで変更すると、それが相手側に反映されるらしい。正直この辺りはどのような実装になっているのか、あるいは、いつのタイミングで変更が反映されるのか、さっぱり理解ができない。しかしまぁ、publicで変更可能でメンバ変数など断じて使うことはないから、動作のメカニズムなど知る必要はないだろう。

非常に素晴らしいではないか! と言いたいところだが、よく調べてみると、.NET Remotingは.NET 3.0以降では非推奨になっているという。今ではWCFを使えと。

WCFは本屋でそういうタイトルの本が売っているのを見たことはあるが、今まで手を出したことがない。とりあえず、統一的に通信できるようにする仕掛けらしいのだが、いまいちよくわからない。かつ、深入りする気力がない。

どうせやりたいこととしては、「同一マシン内で」「お手軽に」プロセス間通信を行いたいだけなのだ。なので、またしてもネットにあったソースをパクってF#に書き直してやってみた。

■WCF版

-------------------------------------------------------------------------------------------

open System
open System.ServiceModel
open System.Diagnostics
open System.Threading

let g_Address = "net.pipe://localhost/test"

// クライアントに公開されるオブジェクトのインタフェース
[<ServiceContract()>]
type public IRemote =
    [<OperationContract(Name="Hello")>]
    abstract member Hello : arg:string -> string

// 遠隔手続きを実装するクラス
type public Remote() =
    interface IRemote with
        member this.Hello( arg ) =
            let pid = Process.GetCurrentProcess().Id
            let thid = Thread.CurrentThread.ManagedThreadId
            printf "Remote.Hello arg=%s, pid=%d, this=%d\n" arg pid thid
            sprintf "Hello %s" arg

// サーバ側の処理
let Server () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Server start pid=%d, this=%d\n" pid thid

    let host = new ServiceHost( typeof<Remote>, new Uri( "net.pipe://localhost" ) )
    host.AddServiceEndpoint(
        typeof<IRemote>,
        new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
        g_Address
    ) |> ignore
    host.Open();

    Threading.Thread.Sleep 60000

// クライアント側の処理
let Client () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Client start pid=%d, this=%d\n" pid thid

    let proxy =
        ChannelFactory.CreateChannel(
            new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
            new EndpointAddress( g_Address )
         )
    
    for i = 1 to 10 do
        printf "client, i=%d, pid=%d, this=%d\n" i pid thid
        proxy.Hello ( "abc" + i.ToString() ) |> ignore
        Threading.Thread.Sleep 1000

[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-------------------------------------------------------------------------------------------

上記と合わせて、System.Runtime.SerializationとSystem.ServiceModelを参照設定に追加してやる。

こうしてやると、概ね.NET Remotingの場合と同じように、クライアント側からメソッドを呼んでやるとサーバ側の手続きが実行されるという挙動が実現できる。

しかし、ここで1つ嵌ったことがあったから記録として残しておく。

上記のコードの内インタフェースの宣言部分について、初めは以下のように書いていた。

[<ServiceContract()>]
type public IRemote =
    [<OperationContract(Name="Hello")>]
    abstract member Hello : string -> string

Helloメソッドの仮引数名を省略していた。

この状態でサーバ側のプロセスを起動すると、new ServiceHost(……)の処理で"ハンドルされていない例外: System.ArgumentNullException: サービス コントラクトを構成する操作内で使用するすべてのパラメーター名は NULL にできません。"なる例外が発生して処理が異常終了する。

どうにも、発生した事象からは原因が特定できず、まともに動作させられるようになるまで、ひどく苦労した。大体、普通、仮想メソッドの仮引数名など意味はないと思うだろう。F#でもC++でも省略できるし。

どうにも原因がわからなくて、とりあえず試しにC#で書いてみたらあっさり動くし、その時何となくIRemote.Helloの仮引数名(arg)が省略できないから、何となくF#側も明記してやったら、たまたま偶然動くようになった。わかるわけねぇだろう。

まぁ、マーシャリングに必要なんだと言われればそんな気もしなくもないが、しかし、エラーメッセージはもう少しどうにかならないものなのだろうか?

0 Comments:

コメントを投稿

Links to this post:

リンクを作成

<< Home