【GUI】入门 Noise(十一):迁移:如何在 CSharp 中嵌入 Racket?
Noise 项目在 Swift 生态中展示了极其优雅的"嵌入式后端"设计。很多 Windows 开发者可能会问:我能在 C# (.NET) 中实现同样的效果吗?
答案是肯定的。而且得益于 C# 强大的互操作性 (P/Invoke) 和 .NET 完善的异步模型,实现起来可能比 Swift 还要顺手。
本文将为你提供一份 C# 版 Noise 架构的实现蓝图,并涵盖实际开发中需要注意的关键细节。
核心挑战与解决方案
要将 Noise 的架构移植到 Windows + C#,我们需要解决三个核心问题:
- 加载运行时: 从
libracketcs.a(Static Lib) 变为libracketcs.dll(Dynamic Lib)。 - 通信管道: 从 Unix File Descriptors 变为 Windows Handles (或 Named Pipes)。
- 数据传输: 在 C# 中实现 NoiseSerde 二进制协议。
第一步:加载 Racket (P/Invoke)
在 Windows 上,你需要编译 Racket CS 生成 libracketcs.dll。有了这个 DLL,我们就可以在 C# 中使用 [DllImport] 加载它。
编译 Racket CS
注意:编译 Racket CS 到 Windows DLL 需要一些额外配置:
# 在 Racket 源码目录中
# 使用 MinGW 或 Visual Studio 编译
# 需要配置 --enable-cs 编译选项
./configure --enable-cs --enable-shared
make
# 编译后会在 racket/lib 目录生成 racketcs.dll
C# 接口声明
你需要声明如下的 C 接口映射:
using System.Runtime.InteropServices;
using System.Text;
internal static class RacketRaw {
// 确保 DLL 在运行目录或系统 PATH 中
const string DLL = "racketcs";
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void racket_boot(ref BootArguments args);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_dynamic_require(IntPtr module, IntPtr sym);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void racket_activate_thread();
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void racket_deactivate_thread();
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void racket_destroy();
// 值创建辅助函数
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_fixnum(long n);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_symbol(IntPtr s);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_string(IntPtr s, ulong len);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_cons(IntPtr a, IntPtr b);
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr racket_nil();
// 字符串管理
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern void racket_free(IntPtr ptr);
[StructLayout(LayoutKind.Sequential)]
public struct BootArguments {
[MarshalAs(UnmanagedType.LPStr)]
public string exec_file;
[MarshalAs(UnmanagedType.LPStr)]
public string boot1_path; // petite.boot
[MarshalAs(UnmanagedType.LPStr)]
public string boot2_path; // scheme.boot
[MarshalAs(UnmanagedType.LPStr)]
public string boot3_path; // racket.boot
}
}
初始化逻辑
在 C# 程序启动时(或者在一个单独的 Thread 中),调用 racket_boot。重要:和 Swift 版一样,初始化 Racket 的线程将成为 Racket 的"主线程",你需要一直维持这个线程存活。
public class RacketRuntime : IDisposable {
private Thread _mainThread;
private volatile bool _running = true;
private bool _initialized = false;
public void Initialize(string bootPath) {
_mainThread = new Thread(() => {
var args = new RacketRaw.BootArguments {
exec_file = bootPath + "\\racket.exe",
boot1_path = bootPath + "\\petite.boot",
boot2_path = bootPath + "\\scheme.boot",
boot3_path = bootPath + "\\racket.boot"
};
RacketRaw.racket_boot(ref args);
_initialized = true;
// 保持线程活跃
while (_running) {
Thread.Sleep(100);
}
RacketRaw.racket_destroy();
}) {
Name = "Racket Main Thread",
IsBackground = false
};
_mainThread.Start();
// 等待初始化完成
while (!_initialized) {
Thread.Sleep(10);
}
}
public void Dispose() {
_running = false;
_mainThread?.Join(TimeSpan.FromSeconds(5));
}
}
第二步:通信机制 (Anonymous Pipes)
这是最有趣的部分。Noise 在 Unix 上使用 pipe() 传递 fd。在 Windows 上,对应的是 Anonymous Pipes (匿名管道)。
C# 端 (Client)
using System.IO.Pipes;
public class PipePair {
public AnonymousPipeServerStream ServerRead { get; }
public AnonymousPipeServerStream ServerWrite { get; }
public IntPtr ClientReadHandle { get; }
public IntPtr ClientWriteHandle { get; }
public PipePair() {
// 创建两个单向管道
// ServerWrite -> ClientRead (C# 写入,Racket 读取)
ServerWrite = new AnonymousPipeServerStream(
PipeDirection.Out,
HandleInheritability.Inheritable
);
ClientReadHandle = ServerWrite.ClientSafePipeHandle.DangerousGetHandle();
// ServerRead <- ClientWrite (Racket 写入,C# 读取)
ServerRead = new AnonymousPipeServerStream(
PipeDirection.In,
HandleInheritability.Inheritable
);
ClientWriteHandle = ServerRead.GetClientHandleAsSafeHandle()
.DangerousGetHandle();
}
public void Dispose() {
ServerWrite?.Dispose();
ServerRead?.Dispose();
}
}
Racket 端 (Server)
Racket CS 在 Windows 上可以很好地处理 OS Handle,但需要特别注意文本模式与二进制模式的区别。
#lang racket/base
(require ffi/unsafe/port)
;; 将 Windows Handle 转换为 Racket Port
(define (windows-handle->port handle mode [options '(binary)])
(unsafe-file-descriptor->port handle mode options))
;; 服务端循环
(define (serve in-handle out-handle)
;; 在 Windows 上必须明确指定 'binary 模式
(define in-port (windows-handle->port in-handle 'in '(binary)))
(define out-port (windows-handle->port out-handle 'out '(binary)))
;; 使用 input/output-bytes 进行二进制读写
(let loop ()
(define req-id (read-uint in-port))
(define func-name (read-string in-port))
(define args (read-args in-port))
;; 处理请求...
(define result (apply func-name args))
;; 写入响应
(write-uint out-port req-id)
(write-result out-port result)
(flush-output out-port)
(loop)))
句柄传递示例
public void StartBackend() {
var pipes = new PipePair();
var runtime = new RacketRuntime();
runtime.Initialize(@"C:\Program Files\Racket");
// 通过 FFI 将句柄传递给 Racket
var inHandle = RacketRaw.racket_fixnum((long)pipes.ClientReadHandle);
var outHandle = RacketRaw.racket_fixnum((long)pipes.ClientWriteHandle);
var modName = ToRacketSymbol("noise-backend");
var serveProc = runtime.Require(modName, ToRacketSymbol("serve"));
// 调用 serve(in-handle, out-handle)
var args = RacketRaw.racket_cons(inHandle,
RacketRaw.racket_cons(outHandle, RacketRaw.racket_nil()));
runtime.Call(serveProc, args);
}
private IntPtr ToRacketSymbol(string s) {
var bytes = Encoding.UTF8.GetBytes(s);
var ptr = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, ptr, bytes.Length);
Marshal.WriteByte(ptr, bytes.Length, 0); // null terminator
var result = RacketRaw.racket_symbol(ptr);
RacketRaw.racket_free(ptr);
return result;
}
第三步:数据协议 (Serialization)
NoiseSerde 的协议(Varint, Length-prefixed string)是语言无关的。在 C# 中,我们可以利用 BinaryReader 和 BinaryWriter 的扩展方法来实现。
Varint 实现
public static class VarintHelper {
// ZigZag 编码:处理负数
public static long EncodeZigZag(long n) {
return (n << 1) ^ (n >> 63);
}
public static long DecodeZigZag(long n) {
return (n >> 1) ^ -(n & 1);
}
// 读取 Varint
public static long ReadVarint(this BinaryReader reader) {
long result = 0;
int shift = 0;
byte b;
do {
b = reader.ReadByte();
result |= (long)(b & 0x7F) << shift;
shift += 7;
} while ((b & 0x80) != 0);
return DecodeZigZag(result);
}
// 写入 Varint
public static void WriteVarint(this BinaryWriter writer, long value) {
var encoded = EncodeZigZag(value);
while (true) {
var b = (byte)(encoded & 0x7F);
encoded >>= 7;
if (encoded == 0) {
writer.Write(b);
break;
} else {
writer.Write((byte)(b | 0x80));
}
}
}
}
基础类型扩展
public static class NoiseWriterExtensions {
public static void WriteString(this BinaryWriter w, string value) {
var bytes = Encoding.UTF8.GetBytes(value);
w.WriteVarint(bytes.Length);
w.Write(bytes);
}
public static string ReadString(this BinaryReader r) {
var len = r.ReadVarint();
if (len == 0) return "";
var bytes = r.ReadBytes((int)len);
return Encoding.UTF8.GetString(bytes);
}
public static void WriteFloat64(this BinaryWriter w, double value) {
var bits = BitConverter.DoubleToUInt64Bits(value);
// Big-endian 编码(与 Swift 版保持一致)
w.Write((byte)(bits >> 56));
w.Write((byte)(bits >> 48));
w.Write((byte)(bits >> 40));
w.Write((byte)(bits >> 32));
w.Write((byte)(bits >> 24));
w.Write((byte)(bits >> 16));
w.Write((byte)(bits >> 8));
w.Write((byte)bits);
}
public static double ReadFloat64(this BinaryReader r) {
var b0 = r.ReadByte();
var b1 = r.ReadByte();
var b2 = r.ReadByte();
var b3 = r.ReadByte();
var b4 = r.ReadByte();
var b5 = r.ReadByte();
var b6 = r.ReadByte();
var b7 = r.ReadByte();
var bits = ((ulong)b0 << 56) |
((ulong)b1 << 48) |
((ulong)b2 << 40) |
((ulong)b3 << 32) |
((ulong)b4 << 24) |
((ulong)b5 << 16) |
((ulong)b6 << 8) |
(ulong)b7;
return BitConverter.UInt64BitsToDouble(bits);
}
}
第四步:异步架构 (Task & Channel)
在 C# 中,我们不需要像 Swift 那样自己实现 Future。我们有现成的 TaskCompletionSource<T>,但需要正确处理错误传播。
错误类型定义
public class RacketException : Exception {
public string RacketErrorMessage { get; }
public RacketException(string message) : base(message) {
RacketErrorMessage = message;
}
}
Backend 类实现
public class RacketBackend : IDisposable {
private readonly ConcurrentDictionary<ulong, TaskCompletionSource<object>> _pending
= new();
private readonly AnonymousPipeServerStream _reader;
private readonly AnonymousPipeServerStream _writer;
private readonly BinaryReader _binaryReader;
private readonly BinaryWriter _binaryWriter;
private readonly CancellationTokenSource _cts = new();
private ulong _sequence = 0;
private Task _readLoopTask;
public RacketBackend(PipePair pipes) {
_reader = pipes.ServerRead;
_writer = pipes.ServerWrite;
_binaryReader = new BinaryReader(_reader, Encoding.UTF8, leaveOpen: true);
_binaryWriter = new BinaryWriter(_writer, Encoding.UTF8, leaveOpen: true);
_readLoopTask = Task.Run(ReadLoop, _cts.Token);
}
private async Task ReadLoop() {
while (!_cts.IsCancellationRequested) {
try {
// 1. 从管道读取 Response ID
var id = _binaryReader.ReadVarint();
// 2. 读取错误标记 (1 = 成功, 0 = 失败)
var success = _binaryReader.ReadByte() == 1;
// 3. 找到对应的 TaskCompletionSource
if (_pending.TryRemove(id, out var tcs)) {
if (success) {
// 读取成功数据
var data = await ReadResultAsync(_binaryReader);
tcs.SetResult(data);
} else {
// 读取错误信息
var errorMsg = _binaryReader.ReadString();
tcs.SetException(new RacketException(errorMsg));
}
} else {
// 未知响应 ID,可能是超时或已取消
// 仍然需要读取并丢弃数据以保持协议同步
await DiscardResultAsync(_binaryReader, success);
}
} catch (OperationCanceledException) {
break;
} catch (Exception ex) {
// 记录错误但继续运行
Console.WriteLine($"Read loop error: {ex}");
}
}
}
private async Task<object> ReadResultAsync(BinaryReader reader) {
// 根据结果类型实现不同的读取逻辑
// 这里简化处理,实际应该根据类型信息来决定
return reader.ReadString(); // 示例:返回字符串
}
private async Task DiscardResultAsync(BinaryReader reader, bool success) {
if (success) {
await ReadResultAsync(reader);
} else {
reader.ReadString();
}
}
public async Task<T> CallRacketAsync<T>(string funcName, params object[] args) {
var id = _sequence++;
var tcs = new TaskCompletionSource<object>();
// 设置超时
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
cts.Token.Register(() => tcs.TrySetCanceled(), false);
_pending[id] = tcs;
try {
// 写入请求到管道
_binaryWriter.WriteVarint(id);
_binaryWriter.WriteString(funcName);
// 写入参数
_binaryWriter.WriteVarint(args.Length);
foreach (var arg in args) {
WriteArgument(_binaryWriter, arg);
}
_binaryWriter.Flush();
var result = await tcs.Task;
return (T)result;
} catch (TaskCanceledException) {
_pending.TryRemove(id, out _);
throw new TimeoutException($"Racket call '{funcName}' timed out");
} catch (Exception ex) {
_pending.TryRemove(id, out _);
throw;
}
}
private void WriteArgument(BinaryWriter writer, object arg) {
switch (arg) {
case string s:
writer.WriteVarint(0); // 类型标记
writer.WriteString(s);
break;
case long l:
writer.WriteVarint(1); // 类型标记
writer.WriteVarint(l);
break;
case double d:
writer.WriteVarint(2); // 类型标记
writer.WriteFloat64(d);
break;
default:
throw new NotSupportedException($"Type {arg.GetType()} is not supported");
}
}
public void Dispose() {
_cts.Cancel();
_readLoopTask?.Wait(TimeSpan.FromSeconds(5));
_cts.Dispose();
_binaryReader?.Dispose();
_binaryWriter?.Dispose();
_reader?.Dispose();
_writer?.Dispose();
}
}
第五步:进阶——代码生成 (The "Magic")
Noise 最酷的地方在于它利用 Racket 的宏系统,根据 definition.rkt 自动生成了 Swift 代码。在 C# 中,我们应该完全复刻这一机制。
扩展 codegen.rkt
最优雅的做法是扩展 Racket 端的 codegen.rkt。查看现有的代码生成逻辑,我们可以添加 C# 代码生成器。
;; 在 codegen.rkt 中添加
(provide
write-Swift-code
write-CSharp-code) ; 新增
;; C# 类型映射
(define (csharp-type t)
((field-type-csharp-proc t)))
(define (write-CSharp-code [out (current-output-port)])
(fprintf out "// Generated by noise-serde-lib~n")
(fprintf out "using System;~n")
(fprintf out "using System.IO;~n")
(fprintf out "using System.Text;~n~n")
;; 生成枚举
(define enum-infos (get-enum-infos))
(for ([id (in-list (sort (hash-keys enum-infos) <))])
(define e (hash-ref enum-infos id))
(fprintf out "~n")
(write-CSharp-enum-code e out))
;; 生成记录
(define record-infos (get-record-infos))
(for ([id (in-list (sort (hash-keys record-infos) <))])
(define r (hash-ref record-infos id))
(fprintf out "~n")
(write-CSharp-record-code r out))
;; 生成 Backend 类
(fprintf out "~n")
(write-CSharp-backend-code out))
;; C# 枚举生成
(define (write-CSharp-enum-code e [out (current-output-port)])
(match-define (enum-info _id name _protocols variants) e)
(fprintf out "public enum ~a {~n" name)
(for ([v (in-vector variants)])
(match-define (enum-variant _id var-name _constructor fields) v)
(if (null? fields)
(fprintf out " ~a,~n" var-name)
(fprintf out " ~a,~n" var-name))) ; 简化处理
(fprintf out "}~n"))
;; C# 记录生成
(define (write-CSharp-record-code r [out (current-output-port)])
(match-define (record-info _id name _constructor _protocols fields) e)
(fprintf out "public record ~a {~n" name)
(for ([f (in-list fields)])
(fprintf out " public ~a ~a { get; init; }~n"
(csharp-type (record-field-type f))
(record-field-id f)))
;; Read 方法
(fprintf out "~n")
(fprintf out " public static ~a Read(BinaryReader reader) {~n" name)
(fprintf out " return new ~a {~n" name)
(for ([f (in-list fields)])
(fprintf out " ~a = Read~a(reader),~n"
(record-field-id f)
(csharp-type (record-field-type f))))
(fprintf out " };~n")
(fprintf out " }~n")
;; Write 方法
(fprintf out "~n")
(fprintf out " public void Write(BinaryWriter writer) {~n")
(for ([f (in-list fields)])
(fprintf out " Write~a(writer, ~a);~n"
(csharp-type (record-field-type f))
(record-field-id f)))
(fprintf out " }~n")
(fprintf out "}~n"))
;; C# Backend RPC 方法生成
(define (write-CSharp-backend-code out)
(define rpc-infos (get-rpc-infos))
(fprintf out "public static class RacketBackendExtensions {~n")
(for ([id (in-list (sort (hash-keys rpc-infos) <))])
(match-define (rpc-info _ rpc-name args result-type _proc)
(hash-ref rpc-infos id))
(fprintf out " public static async Task<~a> ~aAsync(this RacketBackend backend"
(csharp-type result-type)
rpc-name)
;; 参数列表
(for ([arg (in-list args)])
(match-define (rpc-arg label name type) arg)
(fprintf out ",~n ~a ~a" (csharp-type type) name))
(fprintf out ") {~n")
(fprintf out " return await backend.CallRacketAsync<~a>(\"~a\""
(csharp-type result-type) rpc-name)
;; 传递参数
(for ([arg (in-list args)])
(match-define (rpc-arg _ name _) arg)
(fprintf out ",~n ~a" name))
(fprintf out ");~n")
(fprintf out " }~n~n"))
(fprintf out "}~n"))
类型转换器实现
;; 在 serde.rkt 中添加 C# 类型映射
(define field-type-swift-proc
(make-field-type-proc
'(("Float64" . "Float64")
("Float32" . "Float32")
("Int64" . "Int64")
("Int32" . "Int32")
("Int16" . "Int16")
("UVarint" . "UVarint")
("Varint" . "Varint")
("String" . "String")
("Bool" . "Bool"))))
(define field-type-csharp-proc
(make-field-type-proc
'(("Float64" . "double")
("Float32" . "float")
("Int64" . "long")
("Int32" . "int")
("Int16" . "short")
("UVarint" . "ulong")
("Varint" . "long")
("String" . "string")
("Bool" . "bool"))))
使用代码生成器
;; 在你的 Racket 项目中
(require noise-serde-lib/codegen)
;; 生成 C# 代码
(with-output-to-file "RacketBindings.cs"
(λ ()
(write-CSharp-code))
#:exists 'replace)
第六步:性能优化与高级特性
Named Pipes 替代方案
对于高性能场景,可以考虑使用 Named Pipes:
public class NamedPipeBackend : IDisposable {
private NamedPipeServerStream _readPipe;
private NamedPipeServerStream _writePipe;
public NamedPipeBackend(string pipeName) {
_readPipe = new NamedPipeServerStream(
$"{pipeName}_read",
PipeDirection.In,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous
);
_writePipe = new NamedPipeServerStream(
$"{pipeName}_write",
PipeDirection.Out,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous
);
}
// ... 类似的实现
}
内存池优化
public class PooledBuffer : IDisposable {
private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Create(8192);
private byte[] _buffer;
public PooledBuffer(int minimumSize) {
_buffer = _pool.Rent(minimumSize);
}
public byte[] Buffer => _buffer;
public void Dispose() {
if (_buffer != null) {
_pool.Return(_buffer);
_buffer = null;
}
}
}
总结:从 Swift 到 C#
移植 Noise 的核心思想其实就是:
- DLLImport 替代 Bridging Header。
- Windows Handles 替代 File Descriptors,但要注意二进制模式。
- Task/Await 替代 Result/Closure,利用 .NET 强大的异步生态。
- Codegen:扩展 Racket 脚本,直接生成
.cs文件,实现单一数据源。
一旦你打通了这四点,你就拥有了一个在 Windows 上运行的、基于 Racket 的超强脚本后端。无论是用于热更新逻辑、编写复杂的规则引擎,还是嵌入 DSL,这都是一个非常健壮的架构选择。