Julia使用命名管道进行跨进程通信的示例(PowerShell)

其实这个帖子是 Julia如何调用C# dll动态链接库? 的后续。

@northc 一开始我提到可以用Julia通过命名管道调用PowerShell,然后由它代为加载.NET程序集DLL,然后给了个简陋的demo。不过看起来你用着不是很顺利orz,毕竟涉及到Win32的知识比较多…然后我决定写一个完整点儿的↓

命名管道是Windows的一种IPC机制,可以实现双工通信。管道除了默认的字节流模式以外,还有一种消息模式,它会帮我们把每次Write的操作封装成数据报,就不用自己处理边界问题了,挺适合用来搞简易的RPC的。

主要思路是让Julia生成一个随机GUID,然后启动PowerShell,创建一个消息模式的管道,再将每次的调用请求做成文本协议,多次请求即可。Windows提供了一个叫CallNamedPipe的很方便的API,相当于CreateFile/WaitNamedPipeTransactNamedPipeCloseHandle一步到位。

首先是PowerShell侧的服务器代码:

$pipename = $args[0]
$bufsize = 4096
$srv = [System.IO.Pipes.NamedPipeServerStream]::new($pipename, [System.IO.Pipes.PipeDirection]::InOut, 1, [System.IO.Pipes.PipeTransmissionMode]::Message)
if (!$srv) {
    Write-Host "Failed to start pipe server"
    exit(-1)
}
while ($true) {
    $srv.WaitForConnection()
    $inbuf = [byte[]]::new($bufsize)
    $nbread = $srv.Read($inbuf, 0, $bufsize)
    $command = [System.Text.Encoding]::UTF8.GetString($inbuf, 0, $nbread)
    $exit = $false
    $response = switch ($command) {
        "foo" {
            "bar"
        }
        "julia" {
            "lang"
        }
        "exit" {
            $exit = $true
            "bye"
        }
        Default { "Error: unknown command" }
    }
    $outbuf = [System.Text.Encoding]::UTF8.GetBytes($response)
    $srv.Write($outbuf, 0, $outbuf.Length)
    $srv.WaitForPipeDrain()
    $srv.Disconnect()
    if ($exit) {
        exit(0)
    }
}

因为是PowerShell,所以在.NET环境里可以为所欲为了。具体更多玩法可以查看那个帖子。

然后是Julia的代码:

function callnamedpipe(name::String, payload::Vector{UInt8};bufsize = 4096,timeout = 0x00000000)
    outbuf = Vector{UInt8}(undef, bufsize)
    nbread = Ref{UInt32}()
    ret = ccall(:CallNamedPipeA, Int32, (Cstring, Ptr{UInt8}, UInt32, Ptr{UInt8}, UInt32, Ptr{UInt32}, Int32),
    "\\\\.\\PIPE\\$name", payload, length(payload), outbuf, bufsize, nbread, timeout)
    if ret == 0
        return nothing
    else
        return outbuf[1:nbread[]]
    end
end

using UUIDs

pipename = string(uuid4())
p = run(`powershell.exe -File C:\\Users\\Azure\\Desktop\\PipeServer.ps1 $pipename`;wait = false)
while true
    cmd = readline()
    ret = callnamedpipe(pipename, collect(transcode(UInt8, cmd)))
    if !isnothing(ret)
        str = String(ret)
        println(str)
        if str == "bye"
            break
        end
    else
        println("Error calling pipe")
        break
    end
end
> hello
Error: unknown command
> foo
bar
> julia
lang
> exit
bye

剩下的工作就是稍微设计一下文本协议层面的事情了。

PowerShell这边可以分别运行,并不一定非得用Julia启动,只要两边能协商好管道名字即可。管道名字其实也可以是固定的,只是可能有重名风险。callnamedpipetimeout也是可以调的,如果管道未就绪的话它会等待指定的毫秒数。

另外,我最近在做一个Julia直接调C#的包,只不过加班太猛了进度比较慢…

3 个赞

少加班,少熬夜,健康最重要!C#不着急。
我本来不想折腾Windows和C#,可是有些设备不用Windows、C#还不行。
开始准备用Python凑合了,后来发现某个C# dll用pythonnet+PyCall两级FFI的方式找不到部分类型名,用PowerShell就没有这个问题。

PipeServer跑起来了,很强大!
不过Julia的 readline()函数 好像有bug:

  1. 初始任意多个回车会回显绿色的“julia> ”提示符;
  2. 第一行非回车字符(例如 foo)会被吞掉,不发给server,client退出时会吐出来发给REPL,出现红色打印:ERROR: UndefVarError: foo not defined。

我加了一些打印,过程如下:

pipename: 2711ecac-02b4-4494-b641-735cdf96eb81
Please input cmd, ‘exit’ for exit:
julia>

julia> foo

cmd: , collect(transcode(UInt8, cmd)): UInt8
String(payload): , String(outbuf[1:nbread]): Error: unknown command
Error: unknown command
Please input cmd, ‘exit’ for exit:
foo
cmd: foo, collect(transcode(UInt8, cmd)): UInt8[0x66, 0x6f, 0x6f]
String(payload): foo, String(outbuf[1:nbread]): bar
bar
Please input cmd, ‘exit’ for exit:
julia
cmd: julia, collect(transcode(UInt8, cmd)): UInt8[0x6a, 0x75, 0x6c, 0x69, 0x61]
String(payload): julia, String(outbuf[1:nbread]): lang
lang
Please input cmd, ‘exit’ for exit:
exit
cmd: exit, collect(transcode(UInt8, cmd)): UInt8[0x65, 0x78, 0x69, 0x74]
String(payload): exit, String(outbuf[1:nbread]): bye
bye
julia> ERROR: UndefVarError: foo not defined

julia>