惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
WordPress大学
WordPress大学
Help Net Security
Help Net Security
Jina AI
Jina AI
Security Latest
Security Latest
Y
Y Combinator Blog
Project Zero
Project Zero
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
Know Your Adversary
Know Your Adversary
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
NISL@THU
NISL@THU
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
MyScale Blog
MyScale Blog
Cyberwarzone
Cyberwarzone
D
Docker
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
C
CERT Recently Published Vulnerability Notes
B
Blog
L
LangChain Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
SecWiki News
SecWiki News
The Hacker News
The Hacker News
C
Check Point Blog
L
Lohrmann on Cybersecurity
V2EX - 技术
V2EX - 技术
S
Securelist
T
Threat Research - Cisco Blogs
Stack Overflow Blog
Stack Overflow Blog
TaoSecurity Blog
TaoSecurity Blog
云风的 BLOG
云风的 BLOG
Latest news
Latest news
人人都是产品经理
人人都是产品经理
L
LINUX DO - 最新话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
The Register - Security
The Register - Security
Webroot Blog
Webroot Blog
Simon Willison's Weblog
Simon Willison's Weblog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Security Blog
Microsoft Security Blog
AWS News Blog
AWS News Blog
C
Cybersecurity and Infrastructure Security Agency CISA
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
小众软件
小众软件
T
Tailwind CSS Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
宝玉的分享
宝玉的分享
O
OpenAI News

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
将 Rust 绑定到 .NET 10:Oxigraph 的 FFI 桥接实践
张善友 · 2026-06-28 · via 博客园_首页

本文详细介绍了如何将 Oxigraph(一个用 Rust 编写的高性能图数据库)通过 FFI 桥接的方式绑定到 .NET 10,覆盖工具链设计、数据协议、内存管理、回调机制和构建流水线的完整过程。

一、架构概览

┌──────────────────────────────────────────────────────────┐
│  .NET 应用层                                              │
│  ┌──────────┐  ┌────────────────┐  ┌──────────────────┐  │
│  │  Model   │  │     Store      │  │  CustomFunctions │  │
│  │ (record) │  │  (SPARQL+I/O)  │  │  (C#→Rust 回调)  │  │
│  └────┬─────┘  └───────┬────────┘  └────────┬─────────┘  │
│       │                │                    │             │
│  ┌────┴────────────────┴────────────────────┴─────────┐  │
│  │              Interop 层 (FFI/Json)                  │  │
│  │  NativeMethods.g.cs  │  SafeHandles  │  FFIHelper   │  │
│  │  StreamInterop.cs    │  Term JSON Converters        │  │
│  └──────────────────────┬──────────────────────────────┘  │
└─────────────────────────┼─────────────────────────────────┘
                          │ P/Invoke + JSON 字符串
┌─────────────────────────┼─────────────────────────────────┐
│  oxigraph_dotnet.dll    │  (Rust cdylib)                   │
│  ┌──────────────────────┴──────────────────────────────┐  │
│  │  ffi.rs (~2774 行)                                  │  │
│  │  ├─ Store CRUD/Match/SPARQL                         │  │
│  │  ├─ File/Stream/Callback I/O                        │  │
│  │  ├─ Lazy Query Iterator                             │  │
│  │  ├─ Chunked Bulk Loader                             │  │
│  │  ├─ QueryResults Serialization                      │  │
│  │  ├─ Dataset (In-Memory) + Canonicalization          │  │
│  │  └─ Custom Functions / Aggregate                    │  │
│  ├─ stream_ffi.rs: CallbackReader / CallbackWriter     │  │
│  ├─ error.rs:     {"ok":...} / {"error":...} 协议      │  │
│  └─ model_ffi.rs: JSON↔Quad 序列化                     │  │
│  ┌─────────────────────────────────────────────────────┐  │
│  │  oxigraph + oxrdf (Rust crate) + RocksDB            │  │
│  └─────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

核心设计原则:零复制复杂数据类型,全量 JSON 序列化跨 FFI 边界。Rust 侧所有函数签名遵循统一模式:(*const c_char) → *mut c_char,输入和输出都是 JSON 字符串。

二、工具链

2.1 Rust 侧:cdylib

dotnet/src/oxigraph-dotnet/Cargo.toml

[package]
name = "oxigraph-dotnet"
version = "0.5.7"
edition = "2024"

[lib]
crate-type = ["cdylib"]   # ← 编译为动态链接库

[dependencies]
oxigraph = { path = "../../../lib/oxigraph" }
oxrdf = { path = "../../../lib/oxrdf", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[profile.release]
opt-level = "z"            # 体积优化(减小 .dll 尺寸)
lto = true                 # 链接时优化

产物:

  • Windows: oxigraph_dotnet.dll
  • Linux: liboxigraph_dotnet.so
  • macOS: liboxigraph_dotnet.dylib

2.2 .NET 侧:LibraryImport 生成源

.NET 10 使用源生成器 LibraryImport(取代旧的 DllImport)进行 P/Invoke:

// NativeMethods.g.cs — 自动生成的 FFI 绑定
internal static partial class OxigraphNative
{
    private const string LibName = "oxigraph_dotnet";

    [LibraryImport(LibName, EntryPoint = "oxigraph_store_open",
                   StringMarshalling = StringMarshalling.Utf8)]
    internal static partial IntPtr store_open(string path);

    [LibraryImport(LibName, EntryPoint = "oxigraph_store_query",
                   StringMarshalling = StringMarshalling.Utf8)]
    internal static partial IntPtr store_query(IntPtr handle, string queryJson);

    [LibraryImport(LibName, EntryPoint = "oxigraph_register_custom_function",
                   StringMarshalling = StringMarshalling.Utf8)]
    internal static partial IntPtr register_custom_function(
        string name, IntPtr callback);
    // ... 共 60+ 个 FFI 函数
}

关键差异:LibraryImport 是源生成器模式,编译时生成封送代码,避免了 DllImport 的运行时 IL 生成和 AOT 兼容问题。

2.3 构建流水线

build_package.py 编排整个构建过程:

# 步骤 1: 编译 Rust cdylib
cargo build --release -p oxigraph-dotnet --features rocksdb

# 步骤 2: 编译 C# 项目
dotnet build dotnet/ -c Release

# 步骤 3: 将 .dll 拷贝到测试输出目录
# 步骤 4: 运行 xUnit 测试
dotnet test dotnet/tests/Oxigraph.Tests -c Release

三、数据协议:JSON over FFI

3.1 统一响应格式

所有 Rust FFI 函数返回 *mut c_char(JSON 字符串),包含两种可能的格式:

{"ok": <result>}
// 或
{"error": {"kind": "store"|"parse"|"invalid_argument", "message": "..."}}

C# 侧 FFIHelper 统一处理:

// 带返回值的调用
internal static T Call<T>(Func<IntPtr> ffiCall) where T : class
{
    IntPtr jsonPtr = ffiCall();
    string json = ReadAndFree(jsonPtr);  // Marshal.PtrToStringUTF8 + free_string
    ThrowIfError(json);                  // 检查 {"error":...}
    using var doc = JsonDocument.Parse(json);
    return JsonSerializer.Deserialize<T>(
        doc.RootElement.GetProperty("ok").GetRawText())!;
}

// 无返回值的调用
internal static void CallVoid(Func<IntPtr> ffiCall)
{
    IntPtr jsonPtr = ffiCall();
    string json = ReadAndFree(jsonPtr);
    ThrowIfError(json);
}

3.2 错误映射

Rust 错误类型自动映射为 C# 异常:

private static Exception MapError(string kind, string message)
{
    return kind switch
    {
        "store"             => new StoreException(message),
        "parse"             => new ParseException(message),
        "invalid_argument"  => new ArgumentException(message),
        _                   => new OxigraphException($"[{kind}] {message}"),
    };
}

3.3 不透明句柄(Opaque Handles)

Rust 对象不直接暴露给 C#,而是通过指针传递:

// Rust 侧:Store 包装为 Box<UnsafeCell<Store>>,返回指针的 u64 表示
fn store_to_handle(store: Store) -> *mut c_char {
    let boxed = Box::new(UnsafeCell::new(store));
    let ptr = Box::into_raw(boxed);
    let handle_value = ptr as u64;
    // 返回 {"ok":{"handle":12345678}}
    ok_json(&handle_value)
}
// C# 侧:SafeHandle 封装原始指针,确保析构
internal sealed class StoreSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    protected override bool ReleaseHandle()
    {
        OxigraphNative.store_destroy(handle);  // Rust 侧 drop(Box::from_raw(ptr))
        return true;
    }
}

所有句柄类型:

Rust 类型 C# SafeHandle 用途
StoreHandle = *mut UnsafeCell<Store> StoreSafeHandle Store 读写
DatasetHandle = *mut UnsafeCell<Dataset> DatasetSafeHandle Dataset
QuadIterHandle = *mut UnsafeCell<ReaderQuadParser> QuadIterSafeHandle 懒解析迭代器
QueryResultsHandle = *mut QueryResultsWrapper QueryResultsSafeHandle 流式查询结果

四、Stream 回调机制

对于 .NET Stream 的读写(LoadFromStream / DumpToStream),无法简单地把 Stream
传过去,因为 Rust 不认识 .NET 的 Stream 类型。解决方案:C 风格回调

4.1 Rust 侧:实现 Read / Write trait

pub type ReadFn = unsafe extern "C" fn(
    context: *mut c_void, buf: *mut u8, buf_size: i32
) -> i32;

pub struct CallbackReader {
    context: *mut c_void,
    callback: ReadFn,
}

impl Read for CallbackReader {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let result = unsafe {
            (self.callback)(self.context, buf.as_mut_ptr(), buf.len() as i32)
        };
        match result {
            n if n > 0 => Ok(n as usize),
            0 => Ok(0),      // EOF
            -1 => Err(...),  // Error
        }
    }
}

CallbackWriterWrite trait 实现类似。

4.2 C# 侧:GCHandle 固定的委托

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate int ReadCallback(IntPtr context, IntPtr buffer, int bufferSize);

internal sealed class ReadContext : IDisposable
{
    private readonly Stream _stream;
    private readonly byte[] _buffer;
    private readonly GCHandle _gcHandle;  // ← 防止 GC 回收委托

    public readonly IntPtr ContextPtr;
    public readonly ReadCallback Callback;

    public ReadContext(Stream stream)
    {
        _stream = stream;
        Callback = ReadImpl;
        _gcHandle = GCHandle.Alloc(Callback);  // 固定委托
        ContextPtr = (IntPtr)_gcHandle;        // 作为 context 传递
    }

    private int ReadImpl(IntPtr context, IntPtr buffer, int bufferSize)
    {
        try
        {
            int read = _stream.Read(_buffer, 0, Math.Min(bufferSize, _buffer.Length));
            Marshal.Copy(_buffer, 0, buffer, read);
            return read;
        }
        catch { return -1; }
    }

    public void Dispose() { _gcHandle.Free(); }
}

调用链:C# Stream.Read() → 拷贝到 Rust buffer → Rust Read trait → RdfParser.for_reader()

五、自定义函数回调桥

SPARQL 自定义函数需要 Rust 在执行查询时回调到 C#。这是最复杂的 FFI 场景:Rust 调用 C#,C# 再把结果返回给 Rust。

5.1 简单自定义函数

// Rust 侧:函数指针类型
type CustomFnCallback = unsafe extern "C" fn(args_json: *const c_char) -> *mut c_char;

// 全局注册表
static CUSTOM_FUNCTIONS: LazyLock<Mutex<HashMap<String, CustomFnCallback>>> = ...;

#[unsafe(no_mangle)]
pub extern "C" fn oxigraph_register_custom_function(
    name: *const c_char,
    callback: CustomFnCallback,  // C# 函数的指针
) -> *mut c_char {
    CUSTOM_FUNCTIONS.lock().unwrap().insert(name_str, callback);
    ok_json(&"registered")
}

Rust 收到 callback 后,在 SPARQL 评估时调用它:

Rust SPARQL evaluator → 遇到 my:func(?x)
→ 查找 CUSTOM_FUNCTIONS["http://example.com/myFunc"]
→ 调用 callback(args_json)  // ← 跨 FFI 边界回调到 C#
→ 收到返回的 result_json
→ 继续 SPARQL 评估

C# 侧:

// BridgeDelegate:接收 JSON 参数数组,返回 JSON Term
private delegate IntPtr BridgeDelegate(IntPtr argsJsonPtr);

private static IntPtr BridgeImpl(IntPtr argsJsonPtr)
{
    var json = Marshal.PtrToStringUTF8(argsJsonPtr);
    // json: ["http://example.com/myFunc", {"type":"literal","value":"hello"}]

    using var doc = JsonDocument.Parse(json);
    var name = doc.RootElement[0].GetString();
    // 解析剩余元素为 ITerm[]
    var terms = ...;
    // 调用 .NET 函数
    var result = _functions[name](terms);
    // 序列化结果返回给 Rust
    return Marshal.StringToHGlobalAnsi(JsonSerializer.Serialize(result));
}

// 固定 BridgeDelegate,防止 GC 回收
private static readonly GCHandle _gcHandle = GCHandle.Alloc(_bridge);
private static readonly IntPtr _bridgePtr =
    Marshal.GetFunctionPointerForDelegate(_bridge);

5.2 自定义聚合函数

聚合函数需要更复杂的 4 回调协议:

Rust 调用:
  new_fn()     → 返回 ctx handle(GCHandle 包装的 C# 对象)
  acc_fn(ctx, term_json)  → 每行数据调用一次
  finish_fn(ctx) → 返回聚合结果
  free_fn(ctx) → 释放 C# 对象
// Rust 适配器:实现 oxigraph::sparql::AggregateFunctionAccumulator
struct CallbackAggregateAccumulator {
    ctx: *mut c_void,
    acc_fn: AggregateAccCallback,
    finish_fn: AggregateFinishCallback,
    free_fn: AggregateFreeCallback,
}

impl AggregateFunctionAccumulator for CallbackAggregateAccumulator {
    fn accumulate(&mut self, element: Term) {
        let json = serde_json::to_string(&element).unwrap();
        let c_str = CString::new(json).unwrap();
        unsafe { (self.acc_fn)(self.ctx, c_str.as_ptr()) };
    }

    fn finish(&mut self) -> Option<Term> {
        let ptr = unsafe { (self.finish_fn)(self.ctx) };
        // 解析 C# 返回的 JSON Term 或 null
        if ptr.is_null() { return None; }
        let json = unsafe { c_str_to_str(ptr) };
        serde_json::from_str(json).unwrap_or(None)
    }
}

六、RDF 数据类型的 JSON 序列化

Rust 的 serde 格式直接映射到 C# 的 System.Text.Json。两端使用相同的tagged-enum JSON 模式:

// NamedNode
{"type": "uri",     "value": "http://example.com/s"}

// BlankNode
{"type": "bnode",   "value": "b1_abc123"}

// Literal (plain)
{"type": "literal", "value": "hello"}

// Literal (language-tagged)
{"type": "literal", "value": "bonjour", "language": "fr"}

// Literal (typed)
{"type": "literal", "value": "42",
 "datatype": {"type":"uri","value":"http://www.w3.org/2001/XMLSchema#integer"}}

// Triple (RDF-star)
{"type": "triple",
 "subject":   {"type":"uri","value":"http://s"},
 "predicate": {"type":"uri","value":"http://p"},
 "object":    {"type":"uri","value":"http://o"}}

// DefaultGraph
{"type": "default"}

C# 侧自定义 Converter:

public class TermConverter : JsonConverter<ITerm>
{
    public override ITerm? Read(ref Utf8JsonReader reader, ...)
    {
        using var doc = JsonDocument.ParseValue(ref reader);
        var kind = doc.RootElement.GetProperty("type").GetString();
        return kind switch
        {
            "uri"     => new NamedNode(doc.RootElement.GetProperty("value").GetString()!),
            "bnode"   => new BlankNode(doc.RootElement.GetProperty("value").GetString()!),
            "literal" => /* 解析 value/language/datatype/direction */,
            "triple"  => JsonSerializer.Deserialize<Triple>(root.GetRawText())!,
            _ => throw new JsonException($"Unknown term type: {kind}")
        };
    }
}

通过这种 JSON 约定,C# 的 record 类型与 Rust 的 serde 序列化保持了精确对应,无需额外的中间表示层。

七、流式查询结果:懒迭代器

SPARQL 查询可能返回百万级结果。为避免将全部结果物化到一个 JSON 数组中,实现了流式迭代器:

// oxigraph_store_query_iter:返回不透明句柄,不包含结果数据
pub extern "C" fn oxigraph_store_query_iter(
    handle: StoreHandle,
    query_json: *const c_char,
) -> *mut c_char {
    // ... 执行查询,获取 QueryResults
    // 包装为 QueryResultsWrapper 枚举
    // 返回 {"ok":{"handle":...}}  — 不包含任何数据行
}

// 每次取一行
pub extern "C" fn oxigraph_query_iter_next_solution(
    handle: QueryResultsHandle
) -> *mut c_char {
    // 调用 iter.next(),返回单行 JSON 或 null
}

C# 侧 LazySolutionList:按需向 Rust 请求下一行,支持 IReadOnlyList<T> 接口:

private void MaterializeUpTo(int required)
{
    while (_materialized.Count < required)
    {
        var ptr = OxigraphNative.query_iter_next_solution(_handle);
        var json = Marshal.PtrToStringUTF8(ptr) ?? "null";
        OxigraphNative.free_string(ptr);

        if (okVal is null) { _count = _materialized.Count; return; }
        _materialized.Add(new QuerySolution(okVal));
    }
}

八、Chunked Bulk Loader

大数据加载使用 RocksDB 的批量加载路径,以 10,000 个 quad 为一批:

public void BulkExtend(IEnumerable<Quad> quads)
{
    const int chunkSize = 10_000;

    // 1. 开始批量加载器
    var handle = store_bulk_extend_begin();

    // 2. 分批喂入数据
    foreach (var quad in quads)
    {
        chunk.Add(quad);
        if (chunk.Count >= chunkSize) {
            store_bulk_extend_add_chunk(handle, JsonSerializer.Serialize(chunk));
            chunk.Clear();
        }
    }

    // 3. 提交
    store_bulk_extend_add_chunk(handle, JsonSerializer.Serialize(chunk));
    store_bulk_extend_commit(handle);
}

// 异常时自动取消:
// bulkHandle 的 ReleaseHandle 调用 store_bulk_extend_cancel
// Rust 侧:RocksDB BulkLoader
pub extern "C" fn oxigraph_store_bulk_extend_commit(handle) {
    let loader: Box<BulkLoader> = Box::from_raw(handle as *mut _);
    loader.commit()?;
}

九、内存管理

9.1 SafeHandle — 确定性析构

所有 Rust 句柄都包装在 .NET SafeHandle 中。即使进程异常终止(AppDomain 卸载、ThreadAbortException),CLR 也会调用 ReleaseHandle

internal sealed class StoreSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    protected override bool ReleaseHandle()
    {
        OxigraphNative.store_destroy(handle);  // Rust: drop(Box::from_raw(ptr))
        return true;
    }
}

9.2 GCHandle — 防止委托被 GC 回收

当一个 C# 委托被传递给 Rust 侧持有,必须用 GCHandle 固定它:

private static readonly GCHandle _gcHandle = GCHandle.Alloc(_bridge);
private static readonly IntPtr _bridgePtr =
    Marshal.GetFunctionPointerForDelegate(_bridge);

GCHandle.Alloc 确保委托在托管堆上不会被移动或回收,Rust 侧函数指针始终有效。

9.3 字符串生命周期

// Rust 返回的字符串必须由调用方释放
pub extern "C" fn oxigraph_free_string(ptr: *mut c_char) {
    if !ptr.is_null() { unsafe { drop(CString::from_raw(ptr)); } }
}
// C# 侧:每次 FFI 调用后立即释放 Rust 字符串
private static string ReadAndFree(IntPtr ptr)
{
    string json = Marshal.PtrToStringUTF8(ptr);
    OxigraphNative.free_string(ptr);
    return json;
}

十、线程安全

Rust 侧使用 UnsafeCell<Store> 包装 Store,UnsafeCell 不提供任何同步语义:

pub type StoreHandle = *mut UnsafeCell<Store>;

C# 侧明确标注:

/// <summary>
/// An RDF store backed by RocksDB (on disk) or in-memory.
/// Thread safety: not guaranteed. Callers must synchronize concurrent access.
/// </summary>

异步 API 使用 Task.Run 将阻塞操作移到线程池,但不改变线程安全语义。所有对同一 Store 的并发访问(包括 async 方法)必须由调用方串行化。

十一、关键指标

维度 数值
Rust FFI 函数 60+ 个 #[unsafe(no_mangle)]
C# LibraryImport 60+ 个 partial 方法
协议格式 JSON({"ok":...} / {"error":...}
句柄类型 5 种 SafeHandle
回调方向 Rust→C#(CustomFunctions、Stream 回调)
流式查询 懒迭代器,逐行 FFI 调用
构建工具 cargo + dotnet + build_package.py
测试 280 xUnit 测试,全部通过

十二、总结

将 Oxigraph 从 Rust 绑定到 .NET 的核心挑战在于设计一个简单可靠的 FFI 协议。本文采用的方案——JSON over raw C strings + 不透明指针句柄 + GCHandle 固定的回调——在保持代码简洁的同时,提供了完整的跨语言互操作能力。

这一模式适用于任何需要将 Rust 原生库暴露给 .NET 的场景:

  1. cdylib 编译 Rust 到动态链接库
  2. LibraryImport 源生成器做 P/Invoke
  3. 用 JSON 作为数据交换格式(两端的序列化库天然兼容)
  4. SafeHandle + GCHandle 管理跨语言生命周期
  5. 用 C 风格回调实现双向调用
  6. Task.Run 包装提供异步 API