diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 409809d4e..2dd3d5475 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -141,7 +141,11 @@ jobs:
run: pnpm test packages/cli/test/native-output.test.ts
- name: Helper diagnostics and object/link parity
if: matrix.shard == 1
- run: pnpm test packages/compiler/test/native-codegen-integration.test.ts
+ run: >-
+ pnpm test
+ packages/compiler/test/native-codegen-integration.test.ts
+ packages/cli/test/native-link-info.test.ts
+ tests/harness/native-object-example.test.ts
- name: LLVM-tier helper object differential (${{ matrix.shard }}/3)
env:
SCRIPTC_LLVM_HELPER_ONLY: "1"
@@ -167,6 +171,12 @@ jobs:
"$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
--emit=obj -o "$RUNNER_TEMP/installed.o"
file "$RUNNER_TEMP/installed.o" | grep 'Mach-O 64-bit object arm64'
+ "$PREFIX/node_modules/.bin/scriptc" build tests/corpus/001-hello.ts \
+ --print=native-link-info -o "$RUNNER_TEMP/installed-link.o" \
+ > "$RUNNER_TEMP/installed-link.json"
+ node examples/native-object/link.mjs cc \
+ "$RUNNER_TEMP/installed-link.json" "$RUNNER_TEMP/installed-program"
+ test "$("$RUNNER_TEMP/installed-program")" = 'hello world'
# Exercises the supported Windows GNU target and the built CLI end to end:
# TS7 must open its synthetic project, ambient files must resolve across
diff --git a/README.md b/README.md
index c237e1c2e..1219ce3ce 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,13 @@ assembly/object
emission is rejected until the helper's AddressSanitizer pipeline matches the
executable path.
+External object consumption is experimental. Use
+`--print=native-link-info` to emit the object and print a versioned JSON recipe
+containing its target, `main` entry, exact `@scriptc/runtime` source pack,
+required system libraries, FFI inputs, and ABI marker. The recipe never uses
+hidden scriptc cache paths. See [`examples/native-object`](./examples/native-object)
+for C-driver and direct Apple-linker builds.
+
## Use Node APIs
Supported Node APIs compile to the native runtime. For example, `server.ts`:
diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx
index 03d984bc2..0dfbcdf3e 100644
--- a/docs/src/app/cli/page.mdx
+++ b/docs/src/app/cli/page.mdx
@@ -52,6 +52,13 @@ outputs use the matching native helper installed with scriptc and do not
invoke an external compiler, archiver, linker, or SDK. The object is a
relocatable program object with undefined scr_* runtime symbols
and a required scr_runtime_abi_v1 marker, not a standalone library.
+External consumption is experimental and requires the exact runtime version
+reported by --print=native-link-info. That option still writes the
+object, performs no link, and prints a versioned JSON recipe with the target,
+main entry, installed source runtime pack, FFI inputs, and system
+libraries. It never reports private scriptc cache paths. See
+Native Program Objects for complete C-driver and
+direct-linker examples.
--emit=exe is the default and retains the existing executable
behavior.
@@ -83,6 +90,9 @@ Prebuilds the release runtime objects and native TLS/dynamic-engine archives aga
--emit <ir|c|llvm|asm|obj|exe>
Select the invocation's one primary artifact. ir, c, and llvm need only Node. asm and obj use the bundled LLVM helper on macOS 15+ arm64 and emit artifacts targeting macOS 14.0. exe is the default.
+ --print <native-link-info>
+ Build an object (equivalent to --emit=obj) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed source runtime pack and all link inputs, but does not invoke a linker.
+
--dynamic
Embed the dynamic engine (~620KB) so npm dependencies and any-typed code can run. Static stays the default — without this flag, dynamic-tier sites are per-site compile errors. See npm Dependencies.
@@ -182,6 +192,12 @@ An explicit --backend llvm pins the LLVM backend and fails with dia
Bundled scriptc LLVM helper |
Not used |
+
+ External link of --emit=obj with the reported source runtime pack |
+ Not used by the artifact |
+ C compiler required for runtime sources |
+ macOS linker and SDK required |
+
--emit=exe |
Required to run scriptc |
diff --git a/docs/src/app/how-it-works/page.mdx b/docs/src/app/how-it-works/page.mdx
index a38e4a0c5..6e000acce 100644
--- a/docs/src/app/how-it-works/page.mdx
+++ b/docs/src/app/how-it-works/page.mdx
@@ -14,6 +14,13 @@ TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ─
3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On macOS 15+ arm64, `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process helper linked to LLVM 22; it needs no clang or linker and emits macOS 14-targeted artifacts. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production wasm32-wasi target never falls back.
4. **Link** — the runtime is a C library of link-gated feature units: binaries pay only for what they use. A hello-world links nothing but libSystem; a regex-using program links the regex engine; an `http` server links the net stack.
+Program objects define main and leave their selected
+scr_* runtime functions undefined. The
+scr_runtime_abi_v1 reference is a strong link-time compatibility
+check. --print=native-link-info exposes the exact source runtime
+pack and link ordering for external builds; that object ABI is currently
+experimental and exact-runtime-version compatible, not semver-stable.
+
Inspect any stage yourself:
```console
diff --git a/docs/src/app/native-objects/layout.tsx b/docs/src/app/native-objects/layout.tsx
new file mode 100644
index 000000000..3aa7dc660
--- /dev/null
+++ b/docs/src/app/native-objects/layout.tsx
@@ -0,0 +1,7 @@
+import { pageMetadata } from "@/lib/page-metadata";
+
+export const metadata = pageMetadata("native-objects");
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/docs/src/app/native-objects/page.mdx b/docs/src/app/native-objects/page.mdx
new file mode 100644
index 000000000..979bfa9e2
--- /dev/null
+++ b/docs/src/app/native-objects/page.mdx
@@ -0,0 +1,84 @@
+# Native Program Objects
+
+`scriptc build --emit=obj` produces one relocatable macOS arm64 program
+object without invoking clang, a linker, or an SDK. The object defines
+`main`; it is intended to become the program in an external native link. It
+is not a host-callable library—use `scriptc build --lib --profile ...` for
+that interface.
+
+## ABI and runtime contract
+
+The external object ABI is **experimental**. Its `scr_*` function and data
+surface may change before 1.0, so consumers must use the exact
+`@scriptc/runtime` version reported by the same compiler installation. This
+is stricter than semver compatibility.
+
+The object intentionally leaves its selected runtime symbols undefined. It
+also holds a strong reference to `scr_runtime_abi_v1`, which the matching
+runtime defines. Linking an object against a runtime with another ABI marker
+fails at link time with the missing versioned symbol; it cannot become a
+latent runtime incompatibility.
+
+## Machine-readable link information
+
+Add `--print=native-link-info` to emit the object and print a JSON document
+instead of the ordinary path line:
+
+```console
+$ scriptc build main.ts --print=native-link-info -o app.o > link-info.json
+```
+
+The `scriptc.native-link-info.v1` document reports:
+
+- target triple, object format, architecture, minimum OS, and relocation model;
+- the `main` entry and versioned runtime ABI marker;
+- the matching installed `@scriptc/runtime` source-pack root and exact source
+ sets, include paths, defines, and compile flags selected by the program;
+- ordered program, FFI, runtime, and vendor inputs; and
+- required system libraries and frameworks.
+
+Paths inside each source set are relative to `runtime_pack.root`. FFI library
+paths are the manifest-resolved absolute inputs. No path points into scriptc's
+private build cache. The source pack requires a C compiler; the final link
+requires the macOS SDK and linker. Precompiled runtime packs are not shipped
+yet.
+
+## C compiler as linker driver
+
+The repository's `examples/native-object` directory is a runnable example
+with a TypeScript program, a C FFI function, and a small consumer for the JSON
+recipe:
+
+```console
+$ cd examples/native-object
+$ clang -target arm64-apple-macosx14.0.0 -O2 -c native.c -o native.o
+$ scriptc build main.ts --ffi ffi.json --print=native-link-info -o app.o > link-info.json
+$ node link.mjs cc link-info.json app-cc
+$ ./app-cc
+42
+```
+
+The script compiles each reported source set and gives clang only the link
+inputs and system libraries from the document. `--emit=obj` itself remains
+clang-free; this compiler invocation belongs to the external runtime build.
+
+## Native Apple linker
+
+The same example can invoke Apple `ld` directly after compiling the reported
+runtime source sets:
+
+```console
+$ node link.mjs ld link-info.json app-ld
+$ ./app-ld
+42
+```
+
+This lane asks `xcrun` for the selected macOS SDK and linker, then supplies
+the target's minimum OS, every ordered object/archive input, and each reported
+system library. It demonstrates the code-generation boundary precisely:
+scriptc owns `app.o`; an external toolchain owns runtime compilation and the
+platform link.
+
+Outbound FFI declarations retain the same C ABI in clang-compiled LLVM and
+helper-produced object paths. Scalar widths, string/byte pointer-plus-length
+pairs, and callback signatures follow the [Native FFI](/ffi) manifest.
diff --git a/docs/src/app/quickstart/page.mdx b/docs/src/app/quickstart/page.mdx
index 9aeff097e..b69c6a28e 100644
--- a/docs/src/app/quickstart/page.mdx
+++ b/docs/src/app/quickstart/page.mdx
@@ -90,5 +90,6 @@ The package's JS is embedded into the binary at build time — the executable ne
## Next steps
- [CLI Reference](/cli) — every command and flag, including `--emit`, `--backend llvm`, and `--sanitize`.
+- [Native Program Objects](/native-objects) — consume `app.o` from an external C or linker build.
- [Platform Support](/platforms) — cross-compiling to Linux and Windows with zig.
- [Limitations](/limitations) — what doesn't compile yet.
diff --git a/docs/src/lib/docs-navigation.ts b/docs/src/lib/docs-navigation.ts
index c97a05e17..7a5077217 100644
--- a/docs/src/lib/docs-navigation.ts
+++ b/docs/src/lib/docs-navigation.ts
@@ -22,6 +22,7 @@ export const navSections: NavSection[] = [
{ name: "Coverage Reports", href: "/coverage" },
{ name: "npm Dependencies", href: "/dependencies" },
{ name: "Native FFI", href: "/ffi" },
+ { name: "Native Program Objects", href: "/native-objects" },
{ name: "Platform Support", href: "/platforms" },
],
},
diff --git a/docs/src/lib/page-titles.ts b/docs/src/lib/page-titles.ts
index 3d662cf26..55771a959 100644
--- a/docs/src/lib/page-titles.ts
+++ b/docs/src/lib/page-titles.ts
@@ -6,6 +6,7 @@ export const PAGE_TITLES: Record = {
coverage: "Coverage Reports",
dependencies: "npm Dependencies",
ffi: "Native FFI",
+ "native-objects": "Native Program Objects",
platforms: "Platform Support",
"how-it-works": "How It Works",
limitations: "Limitations",
diff --git a/examples/native-object/README.md b/examples/native-object/README.md
new file mode 100644
index 000000000..57fb7fbb5
--- /dev/null
+++ b/examples/native-object/README.md
@@ -0,0 +1,27 @@
+# External program object
+
+This macOS arm64 example links a scriptc program object, a small C FFI
+implementation, and the exact installed source runtime pack. It uses no
+scriptc cache path.
+
+```console
+$ clang -target arm64-apple-macosx14.0.0 -O2 -c native.c -o native.o
+$ scriptc build main.ts --ffi ffi.json --print=native-link-info -o app.o > link-info.json
+$ node link.mjs cc link-info.json app-cc
+$ ./app-cc
+42
+$ node link.mjs ld link-info.json app-ld
+$ ./app-ld
+42
+```
+
+`cc` uses the C compiler as a linker driver. `ld` compiles the same reported
+runtime sources and invokes the Apple linker directly with the selected SDK.
+The object defines `main`; it is a complete program object, not a library to
+load into another process. Use `scriptc build --lib --profile ...` for a
+host-callable static library.
+
+The external object ABI is experimental. Always consume the runtime pack at
+the exact `runtime_pack.version` reported by the same scriptc installation.
+The object requires `scr_runtime_abi_v1`, so a mismatched runtime fails during
+the link instead of starting with an incompatible ABI.
diff --git a/examples/native-object/ffi.json b/examples/native-object/ffi.json
new file mode 100644
index 000000000..bf3e8fecd
--- /dev/null
+++ b/examples/native-object/ffi.json
@@ -0,0 +1,13 @@
+{
+ "ffi_format": 1,
+ "functions": [
+ {
+ "name": "nativeDouble",
+ "symbol": "native_double",
+ "params": ["f64"],
+ "returns": "f64"
+ }
+ ],
+ "libraries": ["./native.o"],
+ "system_libraries": []
+}
diff --git a/examples/native-object/link.mjs b/examples/native-object/link.mjs
new file mode 100644
index 000000000..cba5962e7
--- /dev/null
+++ b/examples/native-object/link.mjs
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+import { spawnSync } from "node:child_process";
+import { mkdirSync, readFileSync, rmSync } from "node:fs";
+import { basename, dirname, join, resolve } from "node:path";
+
+const [mode, infoArg, outputArg] = process.argv.slice(2);
+if ((mode !== "cc" && mode !== "ld") || !infoArg || !outputArg) {
+ console.error("usage: node link.mjs