Skip to content

feat: share the native engine across language bindings - #259

Open
Ayman Bagabas (aymanbagabas) wants to merge 12 commits into
mainfrom
aymanbagabas/shared-ffi
Open

Ayman Bagabas (aymanbagabas) wants to merge 12 commits into
mainfrom
aymanbagabas/shared-ffi

Conversation

@aymanbagabas

@aymanbagabas Ayman Bagabas (aymanbagabas) commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Use one tui-test-ffi library and typed C ABI for Go, Python, and JavaScript.
  • Keep Go on purego, replace Python PyO3 with ctypes, and replace our JavaScript N-API adapter with Koffi.
  • Generate C layouts, conversions, language declarations, and typed async entrypoints from existing Rust definitions.
  • Remove the separate Rust N-API types, conversions, exports, generator, dependencies, and build.
  • Preserve the public language APIs, structured errors, binary screen buffers, and session behavior.

All three bindings accept TUI_TEST_NATIVE_LIBRARY.
Python wheels and JavaScript platform packages include the matching shared library.
Koffi supplies its own native addon; the package is not native-free.

Related work

Related to #255 (test: go binding automation), which adds bundled Go engines, extraction into a local cache, release automation, and C-compiler ABI layout checks.
This PR shares the native engine across languages and generates their ABI declarations.
This work may supersede the Go-only header generation and screenshot ABI fix in #255.
It does not yet replace the bundled Go distribution, cache extraction, or C-compiler layout checks.
Those parts still need integration or a follow-up before this PR can fully supersede #255.

The changes overlap in the Go loader, ABI declarations, and CI/release workflows.
Integration must use tui-test-ffi, its shared-library artifact names, TUI_TEST_NATIVE_LIBRARY, and the generated C header.
The layout checks from #255 should use that header rather than a separate Go-only header generator.
This PR advances the shared contract from ABI version 3 to 4.
The branches' binaries are not interchangeable: #255 changes tui_screenshot in place, while this PR preserves it and adds tui_screenshot_ptr.
Integration must select one final ABI; matching version numbers alone do not make these branch builds compatible.

Authoritative definitions

tools/bindgen reads the existing Rust API, locator, diagnostics, profile, shell, backend, and terminal models.
The C signatures remain authoritative for C entrypoints.
The generator produces the C header, Go declarations, Python declarations, and JavaScript ABI layouts, model metadata, and TypeScript declarations.
It also generates typed asynchronous wrappers from the C signatures.

There is only one generated Rust boundary types.rs.
JavaScript uses generated native/abi.js, native/models.js, and native/index.d.ts.
Handwritten JavaScript handles library selection, promises, validation, transport mappings, and native ownership.
Public Go, Python, and TypeScript convenience APIs remain handwritten.

cargo run --locked -p tui-test-bindgen
cargo run --locked -p tui-test-bindgen -- --check

The check covers generated output.
Regression tests cover source-field, enum, operation, and result propagation, transport mappings, and stale output.

Code size

The full PR at 25254f1bf189 has 8,567 additions and 9,601 deletions: a net reduction of 1,034 lines.
The Koffi step (1ed511273814 to 8b837033dcb5) removes 2,431 lines overall, including generated code and lockfiles.
The counts below cover the shared FFI, generator, and native JavaScript sources.
They include Rust tests but exclude manifests, lockfiles, documentation, and JavaScript tests.
The generator counts as handwritten code.

Source category Before After Change
Handwritten 4,421 3,994 -427
Generated 3,977 3,403 -574

Concurrency and ownership

Direct blocking waits exhausted Koffi's worker pool in a local reproduction.
Thirty-two pending waits delayed an input call by over four seconds.
Typed native start functions copy their inputs before return and execute owned operations independently.
Pending waits cannot prevent input or close calls.
The synchronous C entrypoints remain available to Go and Python.
Immediate getters use Koffi's asynchronous calls without a polling timer.
A bounded submission queue keeps large read batches within Koffi's limits.
Reads remain responsive while long waits are pending.

The adapter copies output strings, arrays, and bytes before tui_result_free.
It releases results and tasks after errors as well as successful calls.
Explicit input retention keeps nested buffers alive through native marshaling.
The function table retains the Koffi library handle, which also avoids a reproduced Bun finalizer crash.
Windows pins the DLL until process exit so Rust thread-local cleanup cannot call unloaded code after a JavaScript environment exits.
Freeing a pending native task abandons its result; it does not cancel the operation.

Packaging and compatibility

The seven existing release targets remain supported.
The build uses Cargo or cargo-zigbuild, not the N-API CLI.
Release assembly generates platform manifests from the shared target map.
It verifies each shared-library artifact and sets matching package versions.
Optional dependencies remain enabled because Koffi also uses them for its platform binaries.

The library and language package must come from the same source revision.
Go users must replace TUI_TEST_GO_NATIVE_LIBRARY with TUI_TEST_NATIVE_LIBRARY and rebuild the renamed library; the old variable has no fallback.
The shared contract uses ABI version 4 for the pointer and asynchronous entrypoints.
Existing synchronous C signatures and layouts remain compatible with their version 3 forms.
Python wheels use py3-none-<platform> tags instead of abi3 tags.
JavaScript remains ESM with Node 20+ support; Bun and Deno remain best effort.

Earlier CI repairs

Go screenshot options now use a generated pointer entrypoint.
This fixes the reproduced AMD64 PureGo by-value argument corruption.
Go generation selects pointer entrypoints when both forms exist.
The raster fixture uses bundled-font characters after its Unicode text and SVG checks.

Validation

Local validation covers Rust formatting, strict Clippy, generator drift, 12 generator tests, 14 FFI tests, all Go packages, and 164 Python tests.
The final JavaScript suite passes 102 tests under Node 20.
Bun and Deno each pass 101 tests with one existing skip.
Linux AMD64 also passes its suite; the short-read optimization has separate concurrency and worker-exit regressions.
Linux musl passes a typed-call and Unicode smoke check.
The packed Node package works without the CLI.
The documented JavaScript example prints Terminal size: 80x30 and Ready 界🙂.

The short-call optimization removes the original one-millisecond getter delay.
A local alternating 200-call sample measured getSize() at a 23-microsecond median, versus 38 microseconds through the previous addon.
These local artifact measurements are a smoke comparison, not a portable performance guarantee.

The first Koffi CI run passed Linux, macOS, and all runtime compatibility jobs.
Windows exposed an input fixture that sent LF instead of terminal Enter.
CodeQL also reported nullable-pointer flows through synchronous tests and the pending-result match.
Commit 9a4addcbf3c0 uses the Enter-aware submit operation, asserts synchronous result pointers, and separates pending and completed result branches.
That run fixed the input fixture but exposed a Windows process-teardown access violation.
Commit 27abbff4b279 replaces raw result dereferences with checked pointer conversions.
Commit 2e222b271520 retains the Windows DLL for process lifetime and adds a worker-only teardown regression.
Local native and integration tests pass all 54 cases; the FFI tests and strict Clippy also pass.
All checks passed at 2e222b271520, including Windows and CodeQL: https://github.com/microsoft/tui-test/actions/runs/38425111081
Commit 25254f1bf189 advances the ABI version to 4 and regenerates all language checks and the C header.
Local validation passes the generator drift check, 14 FFI tests, 22 JavaScript native tests, 10 Python adapter tests, and Go native tests and smoke example.
All checks pass at 25254f1bf189, including Windows, Linux, macOS, runtime compatibility, and CodeQL.
The successful ABI version 4 CI run is https://github.com/microsoft/tui-test/actions/runs/38428880038.

Promote the Go C adapter to tui-test-ffi and generate native declarations from its Rust API. Replace PyO3 with ctypes and load the shared library dynamically from N-API.

Update package assembly, release builds, examples, and ownership coverage. Preserve the supported runtimes and typed binary screen buffers.

Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Comment thread crates/tui-test-ffi/src/output.rs Fixed
Comment thread crates/tui-test-ffi/src/output.rs Fixed
Comment thread crates/tui-test-ffi/src/tests.rs Fixed
Comment thread crates/tui-test-ffi/src/tests.rs Fixed
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad cross-language FFI, ownership, concurrency, and release-packaging changes warrant final human review despite successful automated validation.

0 open findings

What changed in this PR

Unifies Go, Python, and JavaScript bindings around the shared tui-test-ffi C ABI while preserving their public APIs.

Changes:

  • Adds generated ABI layouts, conversions, declarations, and asynchronous task entrypoints.
  • Replaces PyO3 and N-API adapters with ctypes and Koffi.
  • Updates cross-platform builds, packaging, documentation, and regression coverage.
File Description
.github/​scripts/​release/​package-node.mjs Packages shared libraries by platform.
.github/​scripts/​release/​package-node.test.mjs Tests Node package assembly.
.github/​scripts/​release/​prepare-python-linux.sh Prepares Rust/Zig wheel builds.
.github/​workflows/​ci.yml Validates all shared bindings.
.github/​workflows/​release.yml Builds shared-library packages.
.gitignore Ignores generated native artifacts.
Cargo.lock Updates Rust dependencies.
Cargo.toml Adds FFI and bindgen workspace members.
README.md Documents the shared adapter.
bindings/​go/​CONTRIBUTING.md Updates Go development instructions.
bindings/​go/​README.md Documents the shared library setup.
bindings/​go/​integration_test.go Expands screenshot regression coverage.
bindings/​go/​internal/​native/​abi.go Generates Go ABI layouts.
bindings/​go/​internal/​native/​functions.go Generates Go function registrations.
bindings/​go/​internal/​native/​loader.go Loads the shared FFI library.
bindings/​go/​internal/​native/​loader_test.go Updates loader tests.
bindings/​go/​internal/​native/​session.go Uses ABI version and screenshot pointer API.
bindings/​go/​native/​src/​input.rs Removes the Go-specific Rust adapter.
bindings/​js/​.gitignore Ignores shared-library builds.
bindings/​js/​Cargo.toml Removes the N-API crate.
bindings/​js/​README.md Documents Koffi and packaging.
bindings/​js/​build.rs Removes N-API build setup.
bindings/​js/​native/​abi.js Generates Koffi ABI declarations.
bindings/​js/​native/​adapter.js Implements Koffi transport and ownership.
bindings/​js/​native/​index.d.ts Generates native TypeScript declarations.
bindings/​js/​native/​index.js Loads and validates the shared library.
bindings/​js/​native/​lib.rs Removes the N-API implementation.
bindings/​js/​native/​models.js Generates model metadata.
bindings/​js/​package-lock.json Updates JavaScript dependencies.
bindings/​js/​package.json Replaces N-API tooling with Koffi.
bindings/​js/​scripts/​build-native.mjs Builds and copies shared libraries.
bindings/​js/​src/​native.ts Updates native-library error handling.
bindings/​js/​test/​integration.test.mjs Expands runtime compatibility tests.
bindings/​js/​test/​native.test.mjs Tests ownership, concurrency, and loading.
bindings/​js/​test/​options.test.mjs Updates native conversion terminology.
bindings/​python/​.gitignore Ignores packaged shared libraries.
bindings/​python/​MANIFEST.in Defines source-distribution contents.
bindings/​python/​README.md Documents ctypes builds and behavior.
bindings/​python/​examples/​shared_library.py Adds a shared-library example.
bindings/​python/​native/​Cargo.toml Removes the PyO3 crate manifest.
bindings/​python/​native/​src/​lib.rs Removes the PyO3 implementation.
bindings/​python/​pyproject.toml Switches to setuptools-rust.
bindings/​python/​scripts/​generate_stubs.py Removes the old stub generator.
bindings/​python/​setup.py Builds and packages the shared library.
bindings/​python/​src/​tui_test/​_abi.py Generates ctypes ABI declarations.
bindings/​python/​src/​tui_test/​_native.py Implements the ctypes adapter.
bindings/​python/​src/​tui_test/​_native.pyi Updates private adapter types.
bindings/​python/​src/​tui_test/​client.py Removes the obsolete panic probe.
bindings/​python/​src/​tui_test/​py.typed Marks the package as typed.
bindings/​python/​stub-gen/​.gitignore Removes obsolete stub-generator config.
bindings/​python/​stub-gen/​Cargo.lock Removes obsolete stub dependencies.
bindings/​python/​stub-gen/​Cargo.toml Removes the stub-generator crate.
bindings/​python/​stub-gen/​src/​main.rs Removes PyO3 stub generation.
bindings/​python/​tests/​test_ctypes_adapter.py Tests ctypes ownership and workers.
bindings/​python/​tests/​test_integration.py Updates native error regression testing.
crates/​tui-test-ffi/​Cargo.toml Renames the shared FFI crate.
crates/​tui-test-ffi/​README.md Documents ABI generation and ownership.
crates/​tui-test-ffi/​include/​tui_test.h Generates the public C declarations.
crates/​tui-test-ffi/​src/​input.rs Implements shared input conversion.
crates/​tui-test-ffi/​src/​input_generated.rs Generates model input conversions.
crates/​tui-test-ffi/​src/​lib.rs Exposes ABI version 4 and shared entrypoints.
crates/​tui-test-ffi/​src/​output.rs Uses generated output conversions.
crates/​tui-test-ffi/​src/​output_generated.rs Generates output model conversion.
crates/​tui-test-ffi/​src/​session.rs Centralizes session behavior.
crates/​tui-test-ffi/​src/​session_generated.rs Generates session context validation.
crates/​tui-test-ffi/​src/​tasks.rs Implements asynchronous native tasks.
crates/​tui-test-ffi/​src/​tasks_generated.rs Generates typed task entrypoints.
crates/​tui-test-ffi/​src/​tests.rs Adds FFI ownership and validation tests.
crates/​tui-test-ffi/​src/​type_helpers.rs Defines foundational ABI types.
crates/​tui-test-ffi/​src/​types.rs Generates complete ABI structures.
tools/​bindgen/​Cargo.toml Adds generator dependencies.
tools/​bindgen/​src/​boundaries.rs Generates model boundaries and conversions.
tools/​bindgen/​src/​javascript.rs Generates JavaScript models and types.
tools/​bindgen/​src/​koffi.rs Generates Koffi layouts and task wrappers.
tools/​bindgen/​src/​main.rs Orchestrates cross-language generation.
Files not reviewed (1)
  • bindings/js/package-lock.json: Generated file

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants