Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1360e0e

Browse files
committed
Migrating Plugin Architecture to Out-of-Process RPC (MessagePack)
1 parent 944bb93 commit 1360e0e

19 files changed

Lines changed: 759 additions & 259 deletions

File tree

PLUGINS.md

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,31 @@
1-
# f4 Plugin Architecture
1+
# f4 Plugin Architecture (F4-RPC)
22

3-
f4 uses a hybrid, in-process plugin architecture designed to be secure, fast, and cross-platform. It avoids the latency of JSON-RPC while maintaining isolation.
3+
`f4` uses an **Out-of-Process RPC** architecture based on `stdin`/`stdout` and the **MessagePack** binary protocol.
44

5-
## Core Concepts
5+
## Why Out-of-Process?
66

7-
The architecture relies on the "Adapter" (Wrapper) pattern. The application (`f4`) exposes a single, modern Go interface called `HostAPI`.
7+
In earlier versions, `f4` experimented with embedded WASM (`wazero`) and Lua (`gopher-lua`) interpreters to keep plugins strictly in-process while maintaining safety. This approach was abandoned due to several critical flaws:
8+
1. **Binary Bloat**: Embedding multiple interpreters bloated the `f4` core binary.
9+
2. **Platform Access Limitations**: WASM (WASI) severely restricts access to native OS capabilities (raw network sockets, complex file system operations), making it nearly impossible to write powerful plugins like process manager or Widows registry editor without building massive, fragile bridging layers.
10+
3. **Language Lock-in**: Forcing developers to write in a specific dialect of Lua constrained ecosystem growth.
811

9-
Plugins are not executed via `os/exec` or RPC. They run in the same memory space but within sandboxed runtimes.
12+
## The F4-RPC Solution
1013

11-
We support four types of plugins:
12-
1. **Internal Plugins**: Compiled directly into the `f4` binary. Used for critical, performance-sensitive features (e.g., SFTP, VFS).
13-
2. **Modern WASM Plugins**: Written in Go (or Rust/Zig) and compiled to `wasm32-wasi`. They use the modern `f4` API.
14-
3. **far2l Compat WASM Plugins**: Written in C/C++ using the legacy Far Manager / far2l headers.
15-
4. **Lua Scripts**: Loaded via `gopher-lua`. Provides far2m/far3 compatible API (`far.Message`, `far.AdvControl`).
14+
By moving to a separate-process model communicating over standard pipes:
15+
1. **Zero Core Bloat**: The `f4` binary remains extremely lean.
16+
2. **Language Agnostic**: You can write a plugin in Go, Python, Rust, C++, Node.js, or LuaJIT. As long as the process can read/write MessagePack to stdin/stdout, it works.
17+
3. **Unrestricted Power**: Plugins run as native OS processes and can access the network, native libraries, and the full filesystem directly.
18+
4. **Binary Efficiency**: We chose `MessagePack` over `JSON-RPC` to minimize serialization overhead and reduce data transfer latency for large `ReadDir` chunks.
1619

17-
## Why WebAssembly (WASM)?
20+
## How it works
1821

19-
Traditional Far plugins use dynamic libraries (`.dll`, `.so`). This causes several issues:
20-
- **Dependency Hell**: Requires separate builds for Windows, Linux (amd64, arm64), macOS, etc.
21-
- **CGO Requirement**: Forces the Go host to use CGO, complicating cross-compilation.
22-
- **Stability**: A segfault in a C++ plugin crashes the entire file manager.
22+
1. `f4` scans the `plugins/` directory for executable files.
23+
2. It launches each executable via `os/exec`, hooking into its `stdin`, `stdout`, and `stderr`.
24+
3. `stderr` is immediately piped to `f4`'s internal `debug.log` for easy debugging.
25+
4. `f4` sends a `Plugin.Init` request over `stdin`.
26+
5. The plugin replies with its capabilities (e.g., registered Virtual File Systems).
27+
6. When a user interacts with the plugin's VFS, `f4` transparently routes `ReadDir`, `Stat`, `Open`, etc., as RPC calls.
2328

24-
WASM solves this:
25-
- **100% Portability**: One `.wasm` file runs everywhere.
26-
- **Zero CGO**: The `wazero` engine executes WASM natively in Go.
27-
- **Sandboxing**: Memory panics in the plugin are trapped as normal Go errors. The host survives.
29+
## Building a Plugin (Go SDK)
2830

29-
## Far2l C-API Compatibility Layer
30-
31-
To support legacy C/C++ plugins without source modification, we implement a memory thunking bridge in `far2l_compat.go`.
32-
33-
**How it works:**
34-
1. The C plugin is compiled to WASM using `clang --target=wasm32-wasi`.
35-
2. The Go host allocates memory inside the WASM guest's linear memory.
36-
3. The Go host constructs the `PluginStartupInfo` struct inside this memory.
37-
4. The function pointers inside `PluginStartupInfo` point to WebAssembly import indices.
38-
5. These imports are intercepted by `wazero` and routed to Go methods (e.g., `far2lMessage` -> `HostAPI.Message`).
39-
6. Finally, Go calls the WASM exported function `SetStartupInfoW` with the pointer to the struct.
40-
41-
## Future Roadmap
42-
43-
To develop this foundation into a fully-fledged system, the following steps are required:
44-
45-
1. **Guest Memory Management**: Expose `malloc` and `free` from the WASM guest so the Go host can dynamically allocate structs like `PluginPanelItem` and `PluginStartupInfo` safely.
46-
2. **Complete HostAPI**: Extend the `HostAPI` interface with methods for interacting with `vtui` (Dialogs, Menus, InputBoxes).
47-
3. **Virtual File System (VFS)**: Add `GetFindData` and `GetOpenPluginInfo` wrappers to allow plugins to act as virtual panels (e.g., Archives, FTP).
48-
4. **Lua Expansion**: Map the rest of the Lua far3 API to `gopher-lua` tables.
31+
For Go developers, an SDK is provided in `sdk/f4plugin`. It handles all the multiplexing and binary protocol details, exposing a clean, synchronous interface. Check `plugins/dummy/main.go` for a reference implementation.

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ UI & input libraries are developed separately ([vtui](https://github.com/unxed/v
3333
* **Built-in Terminal:** A fully-fledged built-in terminal running underneath the panels, just like `far2l`.
3434
* **Windows Strategy:** First of all, we target recent Windows versions. There are two reasons for that. 1) They support ConPTY. A built-in terminal cannot be implemented properly without it. 2) They have Windows Terminal. So we can avoid the legacy Windows Console API entirely and rely purely on ESC sequence rendering. Windows Terminal supports all we need for proper input, clipboard operations, etc. At the same time, f4's modular architecture makes it possible to implement input/rendering/etc via Windows Console API in future (in fact, our Far-compatible internal architecture is ideally suited for this), so if you want f4 to run on your XP box you will not have to write too much code. Similarly, no one is stopping you from writing a layer for f4's built-in terminal that uses winpty instead of conpty to work on older Windows versions.
3535

36-
### Plugin Architecture (Hybrid In-Process)
36+
### Plugin Architecture (Out-of-Process RPC)
3737

38-
Initially we considered JSON-RPC approach, but rejected it due to possible input lag, so plugins will run within the same address space or host memory:
38+
`f4` uses an ultra-lean, **Out-of-Process** plugin model communicating via `stdin`/`stdout` using the **MessagePack** binary protocol.
3939

40-
1. **WASM (`wazero`):** For heavy system plugins (archivers, VFS, parsers). Write in Go, C, C++, Zig, Rust, etc.—anything that compiles to WASM. Provides 100% portability (a single `.wasm` file for all OSes) and sandboxed security.
41-
2. **Lua (`gopher-lua`):** For fast macros, scripting, and UI customization.
42-
3. **Python:** Just as Lua. Planned for future integration.
43-
4. **API Universality:** The plugin API will ideally support adapter wrappers for *any* existing Far API: Far2, Far3, far2m, and far2l.
44-
5. **Internal Plugins:** The most critical plugins (like network protocols) will be statically linked into the binary but will use the exact same HostAPI as external plugins.
40+
1. **Language Agnostic**: Write plugins in Go, Python, Rust, Node.js, C++, or Lua. If it can speak MessagePack over standard I/O streams, it works.
41+
2. **Native Power**: Because plugins are native external processes, they have full access to the OS (sockets, CGO, external libraries) without the severe restrictions of WASI/WASM sandboxes.
42+
3. **Binary Efficiency**: MessagePack minimizes serialization overhead, preventing the input lag usually associated with JSON-RPC.
43+
4. **Internal Plugins:** The most critical components (like `NetFox` or native VFS) are statically linked into the binary but use the exact same `HostAPI` conceptual interface.
4544

4645
### Special Features
4746

@@ -61,7 +60,7 @@ Initially we considered JSON-RPC approach, but rejected it due to possible input
6160
* Base `f4` UI: Panels, CommandLine, KeyBar, MenuBar.
6261
* `EditorView` powered by an optimized Piece Table (bracketed paste, UTF-8, zero-allocation render).
6362
* Built-in Terminal (`TerminalView` + ANSI Parser + Unix PTY integration).
64-
* Plugin Manager foundation (WASM via wazero, Lua via gopher-lua).
63+
* Plugin Manager foundation.
6564

6665
**Phase 3: Parity & VFS Expansion (Current)**
6766
* Complete remaining standard Far dialogs (Search, Copy/Move, File Attributes, Configuration).

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@ require (
99
github.com/mattn/go-runewidth v0.0.15
1010
github.com/mholt/archives v0.1.5
1111
github.com/pkg/sftp v1.13.6
12-
github.com/tetratelabs/wazero v1.11.0
1312
github.com/unxed/vtinput v0.0.0
1413
github.com/unxed/vtui v0.0.0
15-
github.com/yuin/gopher-lua v1.1.1
14+
github.com/vmihailenco/msgpack/v5 v5.4.1
1615
golang.org/x/crypto v0.21.0
1716
golang.org/x/sys v0.41.0
1817
golang.org/x/term v0.40.0
@@ -27,6 +26,7 @@ require (
2726
github.com/bodgit/windows v1.0.1 // indirect
2827
github.com/dlclark/regexp2 v1.11.4 // indirect
2928
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
29+
github.com/emmansun/base64 v0.9.0 // indirect
3030
github.com/hashicorp/errwrap v1.0.0 // indirect
3131
github.com/hashicorp/go-multierror v1.1.1 // indirect
3232
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -40,6 +40,7 @@ require (
4040
github.com/sorairolake/lzip-go v0.3.8 // indirect
4141
github.com/spf13/afero v1.15.0 // indirect
4242
github.com/ulikunitz/xz v0.5.15 // indirect
43+
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
4344
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
4445
)
4546

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
4646
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
4747
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
4848
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
49+
github.com/emmansun/base64 v0.9.0 h1:92dLrE7iro6g/yWuPsd7M9TzJpe9fEeqKH0H7MApDtE=
50+
github.com/emmansun/base64 v0.9.0/go.mod h1:hp0DxCkKt7bF26HOh4BzhcObvqfH1BVy2vznoGThW6Q=
4951
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
5052
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
5153
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -139,16 +141,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
139141
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
140142
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
141143
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
142-
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
143-
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
144144
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
145145
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
146146
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
147+
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
148+
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
149+
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
150+
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
147151
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
148152
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
149153
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
150-
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
151-
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
152154
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
153155
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
154156
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func main() {
119119
i++
120120
}
121121
case "-test-plugins":
122+
vtui.ConfigDiskLogging(true)
122123
vtui.DebugLog("--- PLUGIN TEST MODE ---")
123124
pm := NewPluginManager()
124125
pm.LoadAll()

plugins.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"sync"
66
"path/filepath"
77
"strings"
8+
"runtime"
89

910
"github.com/unxed/f4/vfs"
1011
"github.com/unxed/f4/plugins/archive"
@@ -82,32 +83,40 @@ func (pm *PluginManager) loadExternal(dir string) {
8283

8384
for _, entry := range entries {
8485
if entry.IsDir() {
85-
// In Far, plugins are usually in subdirectories
86+
// Plugins are usually kept in subdirectories
8687
pm.loadExternal(filepath.Join(dir, entry.Name()))
8788
continue
8889
}
8990

90-
path := filepath.Join(dir, entry.Name())
91+
name := entry.Name()
92+
// Ignore source files and scripts
93+
if strings.HasSuffix(name, ".go") || strings.HasSuffix(name, ".sh") || strings.HasSuffix(name, ".md") {
94+
continue
95+
}
9196

92-
if strings.HasSuffix(entry.Name(), ".lua") {
93-
p := NewLuaPlugin(path)
94-
if err := p.Init(pm.api); err == nil {
95-
pm.mu.Lock()
96-
pm.plugins = append(pm.plugins, p)
97-
pm.mu.Unlock()
98-
vtui.DebugLog("Loaded Lua plugin: %s", p.GetName())
99-
} else {
100-
vtui.DebugLog("Failed Lua plugin %s: %v", path, err)
97+
isExec := false
98+
if runtime.GOOS == "windows" {
99+
if strings.HasSuffix(strings.ToLower(name), ".exe") {
100+
isExec = true
101+
}
102+
} else {
103+
if info, err := entry.Info(); err == nil {
104+
if info.Mode()&0111 != 0 {
105+
isExec = true
106+
}
101107
}
102-
} else if strings.HasSuffix(entry.Name(), ".wasm") {
103-
p := NewWasmPlugin(path)
108+
}
109+
110+
if isExec {
111+
path := filepath.Join(dir, name)
112+
p := NewRPCPlugin(path)
104113
if err := p.Init(pm.api); err == nil {
105114
pm.mu.Lock()
106115
pm.plugins = append(pm.plugins, p)
107116
pm.mu.Unlock()
108-
vtui.DebugLog("Loaded WASM plugin: %s", p.GetName())
117+
vtui.DebugLog("Loaded RPC plugin: %s", p.GetName())
109118
} else {
110-
vtui.DebugLog("Failed WASM plugin %s: %v", path, err)
119+
vtui.DebugLog("Failed RPC plugin %s: %v", path, err)
111120
}
112121
}
113122
}

plugins/dummy/f4-dummy-plugin

2.88 MB
Binary file not shown.

plugins/dummy/main.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/unxed/f4/sdk/f4plugin"
7+
)
8+
9+
type DummyPlugin struct {
10+
host *f4plugin.Host
11+
}
12+
13+
func (p *DummyPlugin) Init(host *f4plugin.Host) ([]string, error) {
14+
p.host = host
15+
ver := host.GetVersion()
16+
host.Log(fmt.Sprintf("Dummy Plugin initialized via F4-RPC! Host Version: %s", ver))
17+
// We deliberately log instead of triggering host.Message to avoid
18+
// annoying popups on application startup.
19+
return []string{"Dummy RPC VFS"}, nil
20+
}
21+
22+
func (p *DummyPlugin) ReadDir(drive, path string) ([]f4plugin.VFSItem, error) {
23+
p.host.Log(fmt.Sprintf("ReadDir called for %s", path))
24+
25+
var items []f4plugin.VFSItem
26+
if path != "/" && path != "" {
27+
items = append(items, f4plugin.VFSItem{Name: "..", IsDir: true})
28+
}
29+
30+
for i := 1; i <= 10; i++ {
31+
items = append(items, f4plugin.VFSItem{
32+
Name: fmt.Sprintf("rpc_file_%d.txt", i),
33+
Size: int64(i * 1024),
34+
IsDir: false,
35+
})
36+
}
37+
items = append(items, f4plugin.VFSItem{Name: "rpc_folder", IsDir: true})
38+
return items, nil
39+
}
40+
41+
func (p *DummyPlugin) Stat(drive, path string) (f4plugin.VFSItem, error) {
42+
p.host.Log(fmt.Sprintf("Stat called for %s", path))
43+
return f4plugin.VFSItem{Name: "item", Size: 1024, IsDir: false}, nil
44+
}
45+
46+
func main() {
47+
f4plugin.Run(&DummyPlugin{})
48+
}

plugins/hello_c/main.c

Lines changed: 0 additions & 18 deletions
This file was deleted.

plugins/hello_go/main.go

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)