diff --git a/.github/workflows/build_binary.yml b/.github/workflows/build_binary.yml index 9bdb0c0..cff3e48 100644 --- a/.github/workflows/build_binary.yml +++ b/.github/workflows/build_binary.yml @@ -17,7 +17,6 @@ jobs: runs-on: ${{ matrix.runner }} env: ASTREIN_VERSION: '2.0.0' - ASTREIN_SHA256: ${{ matrix.astrein-sha256 }} strategy: fail-fast: false @@ -26,14 +25,10 @@ jobs: - target: x86_64-linux-gnu arch: x86_64 runner: ubuntu-24.04 - astrein-archive: astrein-linux-x86_64.tar.gz - astrein-sha256: 'c024f62f8bb4c7b5342249de5844d19276ae92293bf8cc7edf7475280503168e' - target: aarch64-linux-gnu arch: aarch64 runner: ubuntu-24.04-arm - astrein-archive: astrein-linux-aarch64.tar.gz - astrein-sha256: 'bab7501555d91f355bca797a38af16d9e01067591abf20b0147a8a0985558e33' steps: - name: 'Checkout repository' @@ -41,13 +36,6 @@ jobs: with: fetch-depth: 0 - - name: 'Validate ASTrein checksum' - run: | - if [[ ! "${ASTREIN_SHA256}" =~ ^[[:xdigit:]]{64}$ ]]; then - echo "Set the SHA-256 for ${{ matrix.astrein-archive }} after ASTrein v${ASTREIN_VERSION} is released." >&2 - exit 1 - fi - - name: 'Install native compiler' run: | sudo apt-get update @@ -55,19 +43,11 @@ jobs: gcc-14 \ g++-14 - - name: 'Download ASTrein' - run: | - curl --fail --location \ - --output "${RUNNER_TEMP}/${{ matrix.astrein-archive }}" \ - "https://github.com/Katze719/ASTrein/releases/download/v${ASTREIN_VERSION}/${{ matrix.astrein-archive }}" - echo "${ASTREIN_SHA256} ${RUNNER_TEMP}/${{ matrix.astrein-archive }}" \ - | sha256sum --check - - - mkdir -p "${RUNNER_TEMP}/astrein" - tar -xzf "${RUNNER_TEMP}/${{ matrix.astrein-archive }}" \ - -C "${RUNNER_TEMP}/astrein" \ - --strip-components=1 - "${RUNNER_TEMP}/astrein/bin/astrein" --version + - name: 'Setup ASTrein' + id: astrein + uses: Katze719/setup-astrein@v1 + with: + version: ${{ env.ASTREIN_VERSION }} - name: 'Setup CMake' uses: jwlawson/actions-setup-cmake@v2 @@ -81,7 +61,7 @@ jobs: -DCMAKE_C_COMPILER=gcc-14 \ -DCMAKE_CXX_COMPILER=g++-14 \ -DCPP_BINDINGS_LINUX_ENABLE_FFI_JSON_EXPORT=ON \ - -DCPP_BINDINGS_LINUX_ASTREIN_EXECUTABLE="${RUNNER_TEMP}/astrein/bin/astrein" \ + -DCPP_BINDINGS_LINUX_ASTREIN_EXECUTABLE="${{ steps.astrein.outputs.path }}" \ -DCPP_BINDINGS_LINUX_FFI_JSON_OUTPUT="${GITHUB_WORKSPACE}/dist/ffi/${{ matrix.arch }}.ffi.json" - name: 'Generate metadata' diff --git a/README.md b/README.md index 22ebb03..823ab76 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,16 @@ [![Build](https://github.com/Serial-IO/cpp-bindings-linux/actions/workflows/build_binary.yml/badge.svg)](https://github.com/Serial-IO/cpp-bindings-linux/actions/workflows/build_binary.yml) [![JSR](https://jsr.io/badges/@serial/cpp-bindings-linux)](https://jsr.io/@serial/cpp-bindings-linux) -Linux shared library for serial communication. It implements the -[`cpp-core`](https://github.com/Serial-IO/cpp-core) interface and provides functions for discovering, opening, -configuring, reading from, and writing to serial ports. +Runtime-agnostic Linux shared library for serial communication. It implements +the [`cpp-core`](https://github.com/Serial-IO/cpp-core) interface and provides +functions for discovering, opening, configuring, reading from, and writing to +serial ports. + +The library exposes a C-compatible ABI and can be used from any language or +runtime that can load a GNU/Linux shared library and call C functions. Release +artifacts include machine-readable FFI metadata for generating runtime-specific +adapters, including exported symbols, types, callbacks, structs, defaults, and +API documentation. ## Requirements @@ -35,6 +42,31 @@ Official release and JSR artifacts are built for these GNU/Linux targets: | `x86_64-linux-gnu` | generic x86-64 | 2.28 | | `aarch64-linux-gnu` | ARMv8-A | 2.28 | +### Binary compatibility + +The prebuilt binaries require **glibc 2.28 or newer**. Compatibility depends on +the installed glibc version rather than the distribution name. Common release +baselines are shown below for orientation: + +| Distribution | Release baseline | +| --- | --- | +| Debian | 10+ | +| Ubuntu | 20.04 LTS+ | +| RHEL / Rocky Linux / AlmaLinux | 8+ | +| Fedora | 29+ | +| openSUSE Leap | 15.x (not compatible by default) | + +Check the installed version with: + +```sh +ldd --version +``` + +These versions indicate binary compatibility and do not imply that the listed +distribution releases are still supported by their vendors. + +### Portable release builds + The release builds statically include the GNU C++ and compiler runtimes. They still use the target system's glibc and therefore require glibc 2.28 or newer. To reproduce the portable release configuration for the host architecture: @@ -69,7 +101,9 @@ ctest --test-dir build --output-on-failure Tests that require a serial device use `SERIAL_TEST_PORT`. They are skipped when no suitable device is available. -The optional Deno FFI smoke tests require Deno 2 and a built library: +The optional runtime integration smoke tests currently use Deno 2 as their FFI +test harness and require a built library. Deno is not required to consume the +library from another compatible runtime: ```sh cd integration_tests diff --git a/jsr/README.md b/jsr/README.md index 228a6e7..322e815 100644 --- a/jsr/README.md +++ b/jsr/README.md @@ -7,15 +7,45 @@ Binaries are provided as a [package on JSR](https://jsr.io/@serial/cpp-bindings-linux). They are serialized as a base64 string inside the JSON file. +This package targets server-side JavaScript runtimes that can write files and +load GNU/Linux shared libraries. Deno can consume it directly from JSR; Bun and +Node.js use JSR's npm compatibility layer. Browser and edge runtimes cannot use +the native library because they do not expose native FFI access. + The package contains portable binaries for `x86_64-linux-gnu` and `aarch64-linux-gnu`, both requiring glibc 2.28 or newer. The x86-64 artifact uses the generic x86-64 baseline. +## Binary compatibility + +The prebuilt binaries require **glibc 2.28 or newer**. Compatibility depends on +the installed glibc version rather than the distribution name. Common release +baselines are shown below for orientation: + +| Distribution | Release baseline | +| --- | --- | +| Debian | 10+ | +| Ubuntu | 20.04 LTS+ | +| RHEL / Rocky Linux / AlmaLinux | 8+ | +| Fedora | 29+ | +| openSUSE Leap | 15.x (not compatible by default) | + +Check the installed version with: + +```sh +ldd --version +``` + +These versions indicate binary compatibility and do not imply that the listed +distribution releases are still supported by their vendors. + +## FFI metadata + It also includes cpp-core FFI API metadata generated with [ASTrein](https://github.com/Katze719/ASTrein) at `bin/x86_64/ffi.json` and `bin/aarch64/ffi.json`. It describes the exported C symbols, parameter and -return types, callbacks, default values, and API documentation used by -downstream FFI adapter generators. +return types, callbacks, structs, default values, and API documentation used by +runtime-specific FFI adapter generators. This package is primarily intended as a dependency for [`@serial/serial`](https://jsr.io/@serial/serial). However, it can also be used @@ -23,19 +53,139 @@ independently. ## Usage -Import the JSON and write the binary data to disk. Each architecture export also -contains its matching FFI metadata: +Select the export matching the host architecture. Each export contains the +base64-encoded shared library and its matching FFI metadata. The following +examples write the library to disk, load it, and release it again. + +### Deno + +Deno provides native JSR imports and the built-in `Deno.dlopen` FFI API. Save +this as `example.ts`: ```ts -import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; +import { aarch64, x86_64 } from "jsr:@serial/cpp-bindings-linux/bin"; const binary = Deno.build.arch === "aarch64" ? aarch64 : x86_64; -Deno.writeFileSync(`./${binary.filename}`, Uint8Array.fromBase64(binary.data)); +const path = `./${binary.filename}`; + +Deno.writeFileSync(path, Uint8Array.fromBase64(binary.data)); + +const library = Deno.dlopen(path, { + serialOpen: { + parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + result: "i64", + }, +}); +library.close(); +``` + +Run it with write and FFI permissions: + +```sh +deno run --allow-write --allow-ffi example.ts +``` + +### Bun + +Add the package through JSR's npm compatibility layer: + +```sh +bunx jsr add @serial/cpp-bindings-linux +``` + +Then use Bun's built-in `bun:ffi` and `Bun.write` APIs: + +```ts +import { dlopen } from "bun:ffi"; +import { resolve } from "node:path"; +import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; + +const binary = process.arch === "arm64" + ? aarch64 + : process.arch === "x64" + ? x86_64 + : undefined; + +if (!binary) { + throw new Error(`Unsupported architecture: ${process.arch}`); +} + +const path = resolve(binary.filename); +await Bun.write(path, Buffer.from(binary.data, "base64")); + +const library = dlopen(path, { + serialOpen: { + args: ["ptr", "i32", "i32", "i32", "i32", "ptr"], + returns: "i64", + }, +}); +library.close(); +``` + +```sh +bun run example.ts +``` + +> [!WARNING] +> Bun currently marks its built-in +> [`bun:ffi` API](https://bun.sh/docs/runtime/ffi) as experimental. + +### Node.js + +Node.js does not provide a general-purpose C FFI API. This example uses +[Koffi](https://koffi.dev/), together with JSR's npm compatibility layer: -// The matching FFI metadata is available as `binary.ffi`. -// Now you can open the binary using for example `Deno.dlopen`... +```sh +npx jsr add @serial/cpp-bindings-linux +npm install koffi ``` +Save this as `example.mjs`: + +```js +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import koffi from "koffi"; +import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; + +const binary = process.arch === "arm64" + ? aarch64 + : process.arch === "x64" + ? x86_64 + : undefined; + +if (!binary) { + throw new Error(`Unsupported architecture: ${process.arch}`); +} + +const path = resolve(binary.filename); +writeFileSync(path, Buffer.from(binary.data, "base64")); + +const library = koffi.load(path); +library.func("serialOpen", "int64_t", [ + "void *", + "int", + "int", + "int", + "int", + "void *", +]); +library.unload(); +``` + +```sh +node example.mjs +``` + +These examples verify that the native library can be loaded and that its +`serialOpen` symbol can be resolved. The matching `binary.ffi` value describes +the complete set of symbols and structs for generating or configuring +runtime-specific bindings. + +Non-JavaScript consumers can download the same architecture-specific `.so` and +`.ffi.json` files directly from the +[GitHub releases](https://github.com/Serial-IO/cpp-bindings-linux/releases). + > [!NOTE] > For a more in depth guide, check out the > [Wiki](https://github.com/Serial-IO/cpp-bindings-linux/wiki) section on how to diff --git a/jsr/jsr.json b/jsr/jsr.json index c805bb8..8dd42b0 100644 --- a/jsr/jsr.json +++ b/jsr/jsr.json @@ -1,7 +1,7 @@ { "name": "@serial/cpp-bindings-linux", "version": "", - "description": "C++ Linux Bindings for the serial library", + "description": "Runtime-agnostic GNU/Linux serial FFI binaries and API metadata", "license": "LGPL-3.0-only", "exports": { "./bin": "./src/bin/index.ts" diff --git a/jsr/src/bin/index.ts b/jsr/src/bin/index.ts index e73e809..642fae9 100644 --- a/jsr/src/bin/index.ts +++ b/jsr/src/bin/index.ts @@ -2,20 +2,91 @@ * Module that provides serialized binaries and FFI metadata. * * @example - * Import the corresponding binary and write the file to disk. + * Usage with Deno * * ```ts * import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; * * const binary = Deno.build.arch === "aarch64" ? aarch64 : x86_64; + * const path = `./${binary.filename}`; * - * Deno.writeFileSync( - * `./${binary.filename}`, - * Uint8Array.fromBase64(binary.data), - * ); + * Deno.writeFileSync(path, Uint8Array.fromBase64(binary.data)); * - * // The matching FFI metadata is available as `binary.ffi`. + * const library = Deno.dlopen(path, { + * serialOpen: { + * parameters: ["pointer", "i32", "i32", "i32", "i32", "pointer"], + * result: "i64", + * }, + * }); + * library.close(); * ``` + * + * @example + * Usage with Bun + * + * ```ts + * import { dlopen } from "bun:ffi"; + * import { resolve } from "node:path"; + * import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; + * + * const binary = process.arch === "arm64" + * ? aarch64 + * : process.arch === "x64" + * ? x86_64 + * : undefined; + * + * if (!binary) { + * throw new Error(`Unsupported architecture: ${process.arch}`); + * } + * + * const path = resolve(binary.filename); + * await Bun.write(path, Buffer.from(binary.data, "base64")); + * + * const library = dlopen(path, { + * serialOpen: { + * args: ["ptr", "i32", "i32", "i32", "i32", "ptr"], + * returns: "i64", + * }, + * }); + * library.close(); + * ``` + * + * @example + * Usage with Node.js + * + * ```js + * import { writeFileSync } from "node:fs"; + * import { resolve } from "node:path"; + * import koffi from "koffi"; + * import { aarch64, x86_64 } from "@serial/cpp-bindings-linux/bin"; + * + * const binary = process.arch === "arm64" + * ? aarch64 + * : process.arch === "x64" + * ? x86_64 + * : undefined; + * + * if (!binary) { + * throw new Error(`Unsupported architecture: ${process.arch}`); + * } + * + * const path = resolve(binary.filename); + * writeFileSync(path, Buffer.from(binary.data, "base64")); + * + * const library = koffi.load(path); + * library.func("serialOpen", "int64_t", [ + * "void *", + * "int", + * "int", + * "int", + * "int", + * "void *", + * ]); + * library.unload(); + * ``` + * + * The matching C API metadata, including struct definitions, is available as + * `binary.ffi` in every runtime. * @module */ @@ -24,13 +95,41 @@ import aarch64ffi from "../../bin/aarch64/ffi.json" with { type: "json" }; import x86_64Library from "../../bin/x86_64/library.json" with { type: "json" }; import x86_64ffi from "../../bin/x86_64/ffi.json" with { type: "json" }; +/** + * The serialized `aarch64-linux-gnu` shared library and its FFI metadata. + * + * The library targets ARMv8-A and requires glibc 2.28 or newer. Decode `data` + * from base64, write it to `filename`, and load it using the filesystem and + * native FFI APIs provided by your runtime. + */ const aarch64 = { ...aarch64Library, + /** + * ASTrein-generated metadata describing the library's exported C API. + * + * It contains symbols, parameter and return types, callbacks, struct + * definitions, default values, and API documentation for generating + * runtime-specific FFI adapters. + */ ffi: aarch64ffi, }; +/** + * The serialized `x86_64-linux-gnu` shared library and its FFI metadata. + * + * The library targets the generic x86-64 baseline and requires glibc 2.28 or + * newer. Decode `data` from base64 and write it to `filename` before loading + * it using the filesystem and native FFI APIs provided by your runtime. + */ const x86_64 = { ...x86_64Library, + /** + * ASTrein-generated metadata describing the library's exported C API. + * + * It contains symbols, parameter and return types, callbacks, struct + * definitions, default values, and API documentation for generating + * runtime-specific FFI adapters. + */ ffi: x86_64ffi, };