feat(pkg): phase 15 - a package can carry a native library

native "<target>" "<path>" in a package's package.ludic; the compiler records the libraries of
every package a program imports and writes them into the IR (; ludic-native:), so ludicc -o,
ludic build, ludic test and ludic bundle all link one list. macOS: an rpath to the package and to
Contents/Frameworks, where ludic bundle copies and signs each library and drops the build
machine's rpath. Windows: the import library, the .dll copied beside the exe (--natives-out for
the bundle). tools/native/lib.sh builds from a pinned, checksummed source with clang on both
machines; ludic.nativeecho is the worked example; the shim rules are in packages/README.md.
Linked at build time rather than dlopen (docs/PACKAGES.md says why). Reseeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-26 23:35:45 +03:00
parent 5df0747642
commit 5d3a799e33
25 changed files with 54474 additions and 51707 deletions

View file

@ -52,6 +52,39 @@ section. The rules are in [ludic.base](ludic.base/README.md).
| [ludic.lab](ludic.lab/README.md) | the visual lab: a scene on a lit plate, fixed cameras, PNG shots and a contact sheet |
| [ludic.core](ludic.core/) | the canonical engine-ABI components |
| [ludic.prefs](ludic.prefs/) | a small `key=value` store for preferences and records |
| [ludic.nativeecho](ludic.nativeecho/README.md) | the worked example of a package carrying a native library: a C sum over a slice, callbacks drained as facts |
## Native libraries
A package may carry a C or C++ library (`native` lines in its `package.ludic`,
[docs/PACKAGES.md](../docs/PACKAGES.md#native-libraries---a-source-package-that-carries-a-cc-library-phase-15)).
The library is never bound directly: a thin C shim in the package's `native/shim/` is, and every
shim keeps the same shape so the Ludic side stays safe.
- **Only `int`, `long`, `float` and opaque handles cross.** A Ludic `float` is a C `float`, a
`long` an `int64_t`, a `pointer` a `void *`. No struct is passed by value, no C++ type, no
exception: the shim catches and turns it into a return code.
- **A handle is the library's pointer, kept in the package's state and never exported.** The
game names what the package hands it - an int id - and the package maps it to the handle.
- **Nothing calls back into Ludic.** No function pointer crosses the boundary. What a library
reports through a callback (a contact, a finished path, a mixed buffer) the shim records in a
buffer of its own, and the package drains it after the call returns (`ne_scan` then
`ne_drain`), pushing each record onto its facts queue. A record that did not fit is counted,
not dropped silently.
- **Bulk data crosses as a slice the package owns.** A `[]float` or `[]int` passed where the
shim takes a pointer is its data; the package makes it once and reuses it (L7), and passes its
length beside it.
- **Errors are return codes.** 0 is success and a negative number says what failed; nothing
aborts, nothing prints.
- **Threads a library starts stay inside C** (a job system, an audio device). The Ludic side is
called on the game's thread only.
- **The symbols are prefixed** with the package's short name (`ne_`, `jph_`), exported explicitly
(`__attribute__((visibility("default")))`, `__declspec(dllexport)`), and everything else is
hidden.
`native/build.sh` builds `lib/<target>/` from the pinned upstream source and the shim through
[tools/native/lib.sh](../tools/native/lib.sh); run it on the Mac and, in Git Bash, on the PC.
The package's tests run the real library - there is no fake for it.
## The builtin controllers