콘텐츠로 이동

플러그인 모델

Suji의 plugin은 사용자가 만든 cross-cutting 기능을 5 SDK (Zig/Rust/Go/Node + frontend JS)에서 일관되게 호출하기 위한 구조. OS native API (clipboard/fs/dialog 등)는 plugin이 아니라 코어 API로 제공되며, 이 부분은 Electron/Tauri와 동일한 결정.

OS native API사용자 plugin
빌드suji 코어와 함께별도 dylib (또는 source 포함)
5 SDK 노출✅ 자동✅ wrapper 직접 작성
예시clipboard, fs, dialog, tray, menu, notification, globalShortcutstate, sqlite, log, store, http, notification-rich, 사용자 DB / 인증 / telemetry 등
Mac App Sandbox 호환🟡 dylib + entitlements 분리 작업 필요

OS native API를 plugin으로 만들지 않는 이유:

  • Cocoa/CoreFoundation 등 OS framework 링크가 필요해 dylib 분리 시 복잡도가 크게 올라간다
  • CEF Helper 프로세스와 entitlements 격리가 충돌한다
  • Electron/Tauri도 이 부분은 코어 API로 둔다

Tauri의 “plugin install”은 build-time Cargo 의존성이지 런타임 plugin 아님:

Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
Terminal window
cargo build # fetch + compile + link → 같은 binary

→ 사용자가 필요한 것만 install = build-time choice. 결국 같은 binary에 들어감. 진짜 dylib plugin이 아님 (Tauri도 안 함 — OS Sandbox 충돌이 동일).

모델사용자 경험binary 크기진짜 외부 plugin?
Tauricargo add tauri-plugin-fsfeature만큼만❌ build-time dep
Electron(없음, 모든 API 코어)전체 포함
Suji 현재(없음, native API 코어)전체 포함❌ for native API
Suji plugins/<name>/ 패턴plugins/<my-plugin>/ 디렉토리 + Zig dylib사용자 plugin만✅ 진짜 외부 install

진짜 외부 install 형태 plugin은 Suji가 이미 더 강함 — 사용자가 임의 plugin 만들고 dylib으로 빌드, suji.json plugins: ["my-plugin"]에 등록. 5 SDK wrapper도 직접 작성.

plugins/my-plugin/
├── zig/ # plugin 본체 (Zig)
│ ├── build.zig
│ └── src/main.zig # backend_init, backend_handle_ipc, backend_free, backend_destroy
├── rust/ # 5 SDK wrapper (선택)
├── go/
├── node/
└── ts/ # frontend SDK (@suji/<plugin> 또는 inline)

plugins/state/가 첫 공식 — 패턴 참고.

{
"plugins": [
"state",
{
"name": "my-plugin",
"source": "./plugins/my-plugin",
"permissions": ["state:get", "state:set"]
}
]
}

문자열은 로컬/내장 plugins/<name>/을 찾는다. 객체 form의 source는 로컬 경로 또는 GitHub source(github.com/owner/repo, https://github.com/owner/repo)를 받는다. GitHub source는 ~/.suji/plugins/ 아래에 shallow clone/pull 후 빌드한다.

permissions는 플러그인이 init/handler 실행 중 다른 채널로 core.invoke할 때의 outbound allowlist다. 생략하면 기존 호환을 위해 unrestricted, 빈 배열은 deny-all, "*""state:*" 같은 prefix wildcard를 지원한다.

→ suji 시작 시 plugins/my-plugin/zig/zig-out/lib/libbackend.dylib 같은 backend ABI dylib가 자동 빌드/로드된다.

backend dlopen과 같은 entry:

export fn backend_init(core: ?*const SujiCore) callconv(.c) void { ... }
export fn backend_handle_ipc(request: [*:0]const u8) callconv(.c) ?[*:0]u8 { ... }
export fn backend_free(ptr: ?[*:0]u8) callconv(.c) void { ... }
export fn backend_destroy() callconv(.c) void { ... }

→ 5 SDK가 suji.invoke('my-channel', request) 같이 호출.

C/C++ 플러그인은 include/suji.h를 include하면 동일 ABI와 WindowApi v1 raw dispatcher 선언을 사용할 수 있다.

notification-rich는 OS notification primitive를 사용하지만 코어 notification API 표면을 넓히지 않기 위해 공식 plugin으로 제공된다. Windows는 WinRT toast, macOS는 UNUserNotificationCenter action category, Linux는 Freedesktop Notifications D-Bus action signal을 사용하고, action click은 notification:click {notificationId, actionId} 채널로 라우팅된다.

플러그인이 창이나 WebContentsView를 직접 조작해야 할 때는 SujiCore.get_window_api 로 window 전용 C ABI table을 받을 수 있다. 이 table은 window cmd JSON을 받는 request_json dispatcher와 free_response를 제공하며, 주입되지 않은 호스트에서는 invoke("__core__", ...) 경로로 자동 폴백한다.

  • Build-time feature flag: zig build -Dfeatures=fs,clipboard 또는 suji.json features: [...]로 미사용 native API를 binary에서 제외해 크기를 줄일 수 있다. 단 CEF 자체가 binary 크기의 대부분을 차지하므로 절약 효과는 제한적이다.