跳转至

Topic | N-API 原生模块集成模板

重要性:⭐⭐⭐⭐(学 N-API 集成的最佳范本) 出现位置src/native-ts/(3 模块)、vendor/(4 模块) 关联phase-07-advanced.md 的 Native 桥接


1. N-API 是什么

Node-API(N-API):Node.js / Bun 提供的 C/C++ 原生模块 API

为什么需要: - 某些能力纯 JS 做不到:剪贴板读取、键盘事件捕获、GPU 加速、文件索引 - 性能敏感场景:图像处理、压缩、加密 - 平台 API 调用:macOS AVFoundation、Windows Win32 API

2. Claude Code 的 7 个 N-API 模块

src/native-ts/                  # 项目内 N-API 绑定
├── color-diff/                 颜色差异
│   └── index.ts
├── file-index/                 文件索引
│   └── index.ts
└── yoga-layout/                Yoga 布局
    ├── enums.ts
    └── index.ts (2578 行)

vendor/                         # 外部 N-API 绑定(4 模块)
├── audio-capture-src/          音频捕获
│   └── index.ts
├── image-processor-src/        图像处理
│   └── index.ts
├── modifiers-napi-src/         键盘修饰键
│   └── index.ts
└── url-handler-src/            URL scheme handler
    └── index.ts

总计 7 个 N-API 模块TS 侧只写类型type XxxNapi = { ... }),实际实现是 .node 二进制

3. 模板一:audio-capture-src(最完整案例)

3.1 TypeScript 类型定义(20 行)

// vendor/audio-capture-src/index.ts
type AudioCaptureNapi = {
  startRecording(
    onData: (data: Buffer) => void,
    onEnd: () => void,
  ): boolean
  stopRecording(): void
  isRecording(): boolean
  startPlayback(sampleRate: number, channels: number): boolean
  writePlaybackData(data: Buffer): void
  stopPlayback(): void
  isPlaying(): boolean
  // TCC microphone authorization status (macOS only):
  // 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
  // Linux: always returns 3 (authorized) — no system-level microphone permission API.
  // Windows: returns 3 (authorized) if registry key absent or allowed,
  //          2 (denied) if microphone access is explicitly denied.
  microphoneAuthorizationStatus?(): number
}

3.2 注释揭示的"集成设计"

观察注释,揭示了 4 个关键设计:

设计点 注释原文 含义
callback 风格 onData: (data: Buffer) => void N-API 用回调推数据,不用 Promise
TCC 权限码 0=notDetermined, 1=restricted, 2=denied, 3=authorized macOS 系统权限枚举
跨平台 fallback Linux: always returns 3, Windows: registry check 每个平台有自己的处理
可选属性 microphoneAuthorizationStatus?(): number 平台无此 API 时字段缺失

3.3 实际调用(在 React 组件里)

// 推测的实际使用(src/voice/ 下某个组件)
import type { AudioCaptureNapi } from '../../vendor/audio-capture-src/index.js'

let napiModule: AudioCaptureNapi | null = null

// 懒加载 .node 二进制
try {
  napiModule = require('@anthropic-cc/audio-capture')  // 实际 .node 文件
} catch (err) {
  console.warn('Audio capture not available:', err)
}

function useAudioCapture() {
  const [recording, setRecording] = useState(false)

  const start = useCallback(() => {
    if (!napiModule) return

    napiModule.startRecording(
      (data) => { /* 实时音频数据 */ },
      () => { setRecording(false) }
    )
    setRecording(true)
  }, [])

  const stop = useCallback(() => {
    if (!napiModule) return
    napiModule.stopRecording()
  }, [])

  const status = useMemo(() => {
    return napiModule?.microphoneAuthorizationStatus?.() ?? 3
  }, [])

  return { recording, start, stop, status }
}

关键点: - 懒加载 —— 启动时不调,启动后再 require - 容错 —— 没装 .node 也不崩 - Tree-shake 友好 —— 通过 feature() 在不需要的构建里清除

4. 模板二:image-processor-src(可选 API 案例)

4.1 TypeScript 类型

// vendor/image-processor-src/index.ts
export type ClipboardImageResult = {
  png: Buffer
  originalWidth: number
  originalHeight: number
  width: number
  height: number
}

// Clipboard functions are macOS-only and only present in darwin binaries;
// older/non-darwin binaries built before this addition won't export them.
// Typed as optional so callers can guard. These property names appear only
// in type-space here; all runtime property access lives in src/ behind
// feature() so they tree-shake out of builds that don't want them.
export type NativeModule = {
  processImage: (input: Buffer) => Promise<ImageProcessor>
  readClipboardImage?: (maxWidth: number, maxHeight: number) => ClipboardImageResult | null
  hasClipboardImage?: () => boolean
}

4.2 "可选 API" 模式

观察类型里的 ?:: - readClipboardImage? —— 老版本没有 - hasClipboardImage? —— 同上

注释解释: - 老 .node 二进制没这些方法 - 类型用 ?: 表示可能不存在 - 调用方需要运行时检查

if (napiModule.readClipboardImage) {
  const img = napiModule.readClipboardImage(800, 600)
}
- Tree-shake 友好 —— 这些字段不进不需要的构建

5. 模板三:modifiers-napi-src(小工具案例)

// vendor/modifiers-napi-src/index.ts
type ModifiersNapi = {
  // 监听全局键盘修饰键状态
  getModifierState(modifier: 'shift' | 'ctrl' | 'alt' | 'meta'): boolean
  // 注册回调
  onModifierChange(handler: (mods: ModifierState) => void): () => void
}

为什么需要:Ink 拿到的是单个 KeyboardEvent,但有时需要知道"是否按住了 Shift"(用于判断 chord)。

6. 模板四:url-handler-src(协议注册案例)

// vendor/url-handler-src/index.ts
type UrlHandlerNapi = {
  // 注册 claude:// URL scheme
  registerScheme(scheme: string): void
  // 收到 URL 时回调
  onUrlOpened(handler: (url: string) => void): () => void
  // macOS: 让 Claude Code 成为默认 handler
  setAsDefaultHandler(scheme: string): boolean
}

用途: - 用户点 claude://open-session?xxx 链接 - 操作系统调起 Claude Code - 自动跳到对应 session

7. 项目内 N-API 模板:yoga-layout

// src/native-ts/yoga-layout/index.ts (2578 行)
import Yoga from 'yoga-layout'  // N-API 绑定

// 重新导出 + 加 TS 类型 + 包装便利函数
export * from 'yoga-layout'
export type YogaNode = Yoga.YogaNode
export const YGAlign = { /* 枚举包装 */ }
export const YGFlexDirection = { /* 枚举包装 */ }

// 高级 API(推测)
export function layoutInkNode(
  node: InkNode,
  availableWidth: number,
  availableHeight: number
): LayoutResult {
  // 1. 创建 Yoga 节点
  const yogaNode = Yoga.Node.create()

  // 2. 应用 props
  applyProps(yogaNode, node.props)

  // 3. 递归子节点
  for (const child of node.children) {
    layoutInkNode(child, ...)
  }

  // 4. 算最终位置
  yogaNode.calculateLayout(availableWidth, availableHeight)

  return {
    x: yogaNode.getComputedLeft(),
    y: yogaNode.getComputedTop(),
    width: yogaNode.getComputedWidth(),
    height: yogaNode.getComputedHeight(),
  }
}

2578 行 = 完整 Yoga API 包装

8. N-API 集成的"惯用法"

8.1 4 个共同模式

观察 7 个 N-API 模块的 TS 类型,共同模式

  1. callback 异步 —— onDataonEndonModifierChange 等回调
  2. 可选 API —— 老二进制兼容用 ?: 可选属性
  3. 跨平台 fallback —— 注释里说清每个平台行为
  4. Tree-shake 友好 —— 通过 feature() 控制

8.2 调用方 3 步

// Step 1:声明类型
import type { AudioCaptureNapi } from './vendor/audio-capture-src/index.js'

// Step 2:懒加载 + 容错
let napi: AudioCaptureNapi | null = null
try {
  napi = require('@anthropic-cc/audio-capture')  // .node 文件
} catch {}

// Step 3:调用 + 可选检查
if (napi?.microphoneAuthorizationStatus) {
  const status = napi.microphoneAuthorizationStatus()
}

8.3 Bun 怎么 require .node

Bun 运行时自动支持 N-API: - .node 文件在 node_modules 里 - require('xxx') 解析为原生模块 - 不需要 node-gyp / build(编译好的 .node 直接用)

9. 实战:写一个"屏幕截图" N-API 模块

模仿 audio-capture-src 的模式:

// vendor/screen-capture-src/index.ts

export type ScreenCaptureResult = {
  png: Buffer
  width: number
  height: number
}

export type ScreenCaptureNapi = {
  captureScreen(displayId?: number): ScreenCaptureResult | null
  captureWindow(windowId: number): ScreenCaptureResult | null
  // macOS: 需要 Screen Recording 权限(TCC code 0-3)
  // Windows: 需要 admin 或 UAC 同意
  // Linux: 需要 xwd / grim 等外部工具
  hasScreenPermission?(): boolean
  getDisplays(): Array<{ id: number, width: number, height: number }>
}

注释

// TCC screen recording permission (macOS only):
// 0 = notDetermined, 1 = restricted, 2 = denied, 3 = authorized.
// Linux: uses xwd (X11) or grim (Wayland) — always returns true if binary present.
// Windows: uses BitBlt API — no system-level permission required but
//          some apps may have anti-screenshot protection.

10. 关键洞察

10.1 "TS 只写类型" 的好处

  • 类型即文档
  • 编译时类型安全
  • 实现可以替换(不同平台不同二进制)
  • 测试 mock 容易(用 vi.mock 替换)

10.2 "Tree-shake 友好" 的工程意义

  • 外部构建不包含 TCC 权限检查代码
  • bundle size 不膨胀
  • 跨平台构建只包含对应平台代码

10.3 "可选 API" 处理向后兼容

  • 类型 ?: + 运行时 if (xxx) 检查
  • 不用版本号、不用 feature detection
  • JS 的 duck typing 哲学

10.4 N-API 是"性能逃生舱"

  • 大多数 JS 代码够用
  • 性能 / 平台能力瓶颈时,N-API 是唯一选项
  • Claude Code 用了 7 个 = 知道哪些是真瓶颈

11. 阅读清单

  1. vendor/audio-capture-src/index.ts(最完整案例)
  2. vendor/image-processor-src/index.ts(可选 API 模式)
  3. src/native-ts/yoga-layout/index.ts:1-80(项目内 N-API)
  4. src/native-ts/file-index/(文件索引 N-API)
  5. src/native-ts/color-diff/(颜色差异 N-API)
  6. 📌 vendor/modifiers-napi-src/index.ts + url-handler-src/index.ts

12. 练习任务

  1. 设计一个屏幕截图 N-API 模块的类型定义 —— 模仿 audio-capture-src 的格式
  2. 实现一个 mock —— 用普通 JS 模拟 N-API(用 setTimeout 模拟回调)
  3. 写懒加载包装 —— 容错的 try { require } catch {} 模式
  4. 思考:为什么 Claude Code 选择 N-API 而不是 Wasm?N-API vs Wasm 的 trade-off 是什么?