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

@ -439,7 +439,7 @@ function emit_cov_runtime() -> void {
# drivers read); with a path it is written to that file so ludicc can hand it to
# clang itself.
function ir_flush(path: pointer) -> bool {
let h = buf_str(head)
let h = native_ir_lines() + buf_str(head)
let c = buf_str(code)
if (path == null) { # raw IR to stdout (no trailing newline)
let out = file_stdout()

View file

@ -0,0 +1,105 @@
# natives.ludic - a package can carry a native library (phase 15).
#
# A package's package.ludic says `native "<target>" "<path>"` for each library it links, per
# target. When an import is found in a package, its manifest is read once and the libraries for
# this build's target are kept, as absolute paths. ir_flush writes them into the IR as
# `; ludic-native: <path>` lines, so every link - ludicc's own, `ludic build`, `ludic test` and
# `ludic bundle` - reads one list, and a program that imports no such package links nothing new.
var g_native_libs: []pointer = new []pointer
var g_native_pkgs: []pointer = new []pointer
# the target a `native` line names: windows-x64, macos-arm64 or linux-x64.
# $LUDIC_NATIVE_TARGET names another (an Intel Mac says macos-x64).
function native_target() -> pointer {
let env = getenv("LUDIC_NATIVE_TARGET")
if (env != null) { return env }
if g_target_win { return "windows-x64" }
if Fs.is_dir("/System/Library") { return "macos-arm64" }
return "linux-x64"
}
# the words of one manifest line, a quoted word as one word without its quotes
function native_words(line: pointer) -> []pointer {
let out = new []pointer
let n = len(line)
var i = 0
while i < n {
while (i < n) and ((line[i] == ' ') or (line[i] == 9)) { i += 1 }
if (i >= n) or (line[i] == '#') { return out }
if line[i] == '"' {
let s = i + 1
i = s
while (i < n) and (line[i] != '"') { i += 1 }
push(out, line[s..i])
i += 1
} else {
let s = i
while (i < n) and (line[i] != ' ') and (line[i] != 9) { i += 1 }
push(out, line[s..i])
}
}
return out
}
function native_have(xs: []pointer, s: pointer) -> bool {
var i = 0
while i < len(xs) { if xs[i] == s { return true }; i += 1 }
return false
}
# `full` was found as `rel` under a package root (ludic_modules or $LUDIC_HOME/packages):
# read that package's manifest once and keep its libraries for this target
function native_note_pkg(full: pointer, rel: pointer) -> void {
var n = 0
while (n < len(rel)) and (rel[n] != '/') { n += 1 }
let root = full[0..len(full) - len(rel) + n] + "/"
if native_have(g_native_pkgs, root) { return }
push(g_native_pkgs, root)
let txt = read_file(root + "package.ludic")
if (txt == null) { return }
let want = native_target()
var i = 0
let tn = len(txt)
while i < tn {
var e = i
while (e < tn) and (txt[e] != '\n') { e += 1 }
var le = e
if (le > i) and (txt[le - 1] == 13) { le -= 1 }
let ws = native_words(txt[i..le])
if (len(ws) == 3) and (ws[0] == "native") and (ws[1] == want) {
let lib = root + ws[2]
if not native_have(g_native_libs, lib) { push(g_native_libs, lib) }
}
i = e + 1
}
}
# the `; ludic-native:` lines ir_flush puts at the head of the IR
function native_ir_lines() -> pointer {
var s = ""
var i = 0
while i < len(g_native_libs) { s = s + `; ludic-native: {g_native_libs[i]}\n`; i += 1 }
return s
}
# what ludicc's own link adds: on macOS the library and an rpath to its directory (and to where
# a bundle keeps it); on Windows the import library beside the .dll, the .dll copied beside the exe
function native_link_args(out: pointer) -> pointer {
var s = ""
var i = 0
while i < len(g_native_libs) {
let lib = g_native_libs[i]
let dir = dir_of(lib)
if g_target_win {
let n = len(lib)
s = s + ` "{lib[0..n - 4]}.lib"`
var od = dir_of(out)
if len(od) == 0 { od = "." }
run(`copy /y "{win_path(lib)}" "{win_path(od)}" >nul 2>nul`)
} else {
s = s + ` "{lib}" -Wl,-rpath,{dir} -Wl,-rpath,@executable_path/../Frameworks`
}
i += 1
}
return s
}

View file

@ -1228,6 +1228,7 @@ function do_import(rel: pointer) -> void {
if is_runtime_path(rel) or not beside or (beside and unsafe_trusted(g_parse_file)) { trust_file(full) }
if beside and not is_runtime_path(rel) and not (module_of(g_parse_file) == "") { module_set(full, module_of(g_parse_file)) }
if not beside and not is_runtime_path(rel) { pkg_file_set(full, pkg_name_of(rel)) } # modules.ludic
if not beside and not is_runtime_path(rel) and (src != null) { native_note_pkg(full, rel) } # natives.ludic
if beside and not is_runtime_path(rel) and not (pkg_of_file(g_parse_file) == "") { pkg_file_set(full, pkg_of_file(g_parse_file)) }
if (src == null) { perr(`cannot open import {full}`) }
# the audio runtime can arrive through atlas.ludic's own import or the Assets

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -138,6 +138,7 @@ entry {
var title = null # --title <name>: game_title() - the window's first title - instead of the program name
var gui = false # --gui: a Windows GUI-subsystem program (no console behind the window)
var links = new []pointer # --link <file>: an extra input to the link (a compiled resource)
var natives_out = null # --natives-out <file>: the package libraries linked, one a line (ludic bundle)
g_coverage = false # --coverage: instrument each statement with a per-line hit counter
var ai = 1
@ -166,6 +167,7 @@ entry {
else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
else if a == "--gui" { gui = true }
else if a == "--title" { ai += 1; if ai < arg_count() { title = arg(ai) } }
else if a == "--natives-out" { ai += 1; if ai < arg_count() { natives_out = arg(ai) } }
else if a == "--link" { ai += 1; if ai < arg_count() { push(links, arg(ai)) } }
else if a == "-o" { ai += 1; if ai < arg_count() { out = arg(ai) } }
else if a[0] == '-' {
@ -359,6 +361,8 @@ entry {
# console-subsystem one opens a console window behind the game. mainCRTStartup keeps
# the C runtime's ordinary main() entry rather than WinMain.
if g_target_win and gui { cmd = `{cmd} -Wl,/subsystem:windows -Wl,/entry:mainCRTStartup` }
# a package's native libraries (natives.ludic)
cmd = cmd + native_link_args(out)
var li = 0
while li < len(links) { cmd = `{cmd} "{links[li]}"`; li += 1 }
cmd = `{cmd} -o {out}`
@ -371,6 +375,13 @@ entry {
let rc = run(line)
if not save { remove_file(ll) }
if rc != 0 { die("ludicc: link failed\n") }
if natives_out != null {
var nl = ""
var ni = 0
while ni < len(g_native_libs) { nl = nl + g_native_libs[ni] + "\n"; ni += 1 }
let nf = file_open(natives_out, "wb")
if nf != null { file_write(nf, nl, len(nl)); file_close(nf) }
}
# run mode: execute the binary we just built and forward its exit code.
# system() hands back a wait status on POSIX and the exit code itself on Windows.