菜单

原生模块集成

相关源文件

本文档介绍了 AFFiNE 中原生 Rust 模块如何集成到 JavaScript 前端和后端。原生模块提供了性能关键的功能,例如媒体捕获和存储操作,这些功能如果仅用 JavaScript 实现将效率低下或不可能实现。

架构概述

AFFiNE 通过 Rust 模块实现原生功能,这些模块被编译成特定于平台的二进制文件,并通过各种绑定机制暴露给 JavaScript。

原生模块结构

来源: Cargo.toml2-76 packages/frontend/native/package.json1-48 packages/backend/native/package.json1-42

原生模块包

AFFiNE 实现了三个主要的原生模块包

目的主要平台
@affine/native用于 Electron 和 Web 的前端原生模块macOS、Windows、Linux
@affine/server-native用于 Node.js 服务器的后端原生模块Linux、macOS、Windows
@affine/mobile-native移动原生模块Android

来源: Cargo.toml2-11 packages/frontend/native/package.json1-48

前端原生模块 (@affine/native)

@affine/native 包提供了以下主要功能

媒体捕获系统

媒体捕获功能允许应用程序在 macOS 上录制系统应用程序的音频

主要类

  • ShareableContent: 管理屏幕/音频捕获权限和设备访问
  • TappableApplication: 表示其音频可被捕获的应用程序
  • AudioTapStream: 管理音频捕获流
  • Application: 表示一个系统应用程序

来源: packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs98-526 packages/frontend/native/media_capture/src/macos/tap_audio.rs389-458

文档存储系统

文档存储功能提供了到 SQLite 数据库的原生接口,用于存储文档数据

主要类

  • DocStorage: 管理工作区的文档存储
  • DocStoragePool: 管理多个文档存储实例
  • SqliteConnection: 提供对 SQLite 数据库的低级访问

来源: packages/frontend/native/index.d.ts26-111

后端原生模块 (@affine/server-native)

@affine/server-native 包为 Node.js 后端提供了原生功能

主要功能

  • 文本处理和分词
  • 文档解析和转换
  • 加密操作
  • 通过 y-octo 进行 Y.js 文档操作

来源: Cargo.lock155-173 packages/backend/native/package.json1-42

移动原生模块 (Android)

AFFiNE 的移动支持使用 UniFfi 绑定生成器进行 Rust 到 Kotlin 的绑定

Android 集成使用 Mozilla 的 Rust-Android Gradle 插件来编译 Rust 到 Android 目标,并使用 UniFfi 进行绑定生成。

来源: packages/frontend/apps/android/App/build.gradle142-187 packages/frontend/apps/android/App/gradle/libs.versions.toml33-100

构建原生模块

构建配置

原生模块使用 NAPI-RS 进行 Node.js 绑定构建,负责编译 Rust 代码和生成 JavaScript 绑定

构建配置定义在多个地方

  1. Cargo.toml: 工作区定义和 Rust 依赖
  2. package.json: NAPI-RS 构建配置和目标平台
  3. .cargo/config.toml: 特定于平台的 Rust 标志
  4. .github/actions/build-rust/action.yml: CI 构建过程

来源: Cargo.toml1-88 .cargo/config.toml1-15 packages/frontend/native/package.json6-16 .github/actions/build-rust/action.yml18-91

平台目标

原生模块支持多个平台和架构

平台架构
macOSx86_64 (Intel),aarch64 (Apple Silicon)
Windowsx86_64, aarch64, ia32
Linuxx86_64, aarch64 (gnu/musl 变体)
Androidaarch64

来源: packages/frontend/native/package.json8-14 packages/backend/native/package.json20-27

运行时加载

在运行时,会根据平台和架构动态加载合适的原生二进制文件

index.js 中的加载逻辑会尝试多个文件路径来查找适合当前平台和架构的二进制文件。如果本地找不到二进制文件,它会回退尝试单独发布的、包含特定平台二进制文件的 npm 包。

来源: packages/frontend/native/index.js66-375

JavaScript API 接口

原生模块通过 TypeScript 定义暴露一致的 JavaScript API

来源: packages/frontend/native/index.d.ts1-210

媒体捕获实现

媒体捕获功能是一个复杂的原生模块,它与 macOS API 交互

主要实现细节

  • 使用 macOS ScreenCaptureKit 进行应用程序检测
  • 使用 CoreAudio API 进行音频捕获
  • 使用 SIMD 优化进行音频缓冲区处理
  • 使用线程管理进行音频回调传递

来源: packages/frontend/native/media_capture/src/macos/screen_capture_kit.rs98-347 packages/frontend/native/media_capture/src/macos/tap_audio.rs237-326 packages/frontend/native/media_capture/src/macos/audio_buffer.rs18-141

使用示例

媒体捕获示例

文档存储示例

挑战和考虑因素

跨平台兼容性

  • 必须条件性地编译特定于平台的库和 API
  • 平台特定功能的特性检测和回退
  • 尽管实现存在差异,但在平台之间保持一致的 API

性能

  • 最小化 JavaScript 和 Rust 之间的数据拷贝
  • 在可用时使用 SIMD 优化
  • 仔细的内存管理以避免泄漏

构建流程

  • 构建多平台的复杂CI设置
  • 巨大的二进制文件大小,需要仔细的发行策略
  • AFFiNE 版本之间的版本控制和兼容性

来源:.github/actions/build-rust/action.yml18-91 .cargo/config.toml1-15

结论

原生模块集成通过利用 Rust 作为实现语言,以及各种用于 JavaScript 集成的绑定技术,为 AFFiNE 提供了关键的性能优势,例如在媒体捕获和文档存储等操作中。该架构在跨平台兼容性和原生性能之间取得了平衡。

模块化设计允许进行特定平台的优化,同时在不同环境中保持一致的 API,使 AFFiNE 能够在以 Web 为先的应用程序架构中提供类原生的性能。