feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show

Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.

Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.

App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-17 13:32:23 +03:00
parent c1db0d71ed
commit 43d5eb5b88
31 changed files with 89274 additions and 86727 deletions

View file

@ -258,6 +258,14 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if g_windowed { emit(` call void @lp_app_icon(ptr {ip})\n`) }
return val("0", "void")
}
if (meth == "window_hide") or (meth == "window_show") {
# Hide / show the game's own window (not the splash) without tearing down its
# GL / Vulkan surface - a launcher steps aside while the game it started runs.
# Headless there is no window, and the call lowers to nothing.
use_pak()
if g_windowed { emit(` call void @app_{meth}()\n`) }
return val("0", "void")
}
perr(`unknown builtin App.{meth}`)
}
if (ns == "Pool") {
@ -516,6 +524,9 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "header") { bare = "http_header_of"; push(labels, "handle"); push(labels, "name") }
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
if (meth == "parse") { bare = "http_parse"; push(labels, "bytes"); push(labels, "len") }
if (meth == "save_to") { bare = "http_save_to"; push(labels, "handle"); push(labels, "path") }
if (meth == "received") { bare = "http_received"; push(labels, "handle") }
if (meth == "expected") { bare = "http_expected"; push(labels, "handle") }
}
# Udp.* — polled IPv4 datagrams (runtime/native/udp.ludic + udp.ll / udp_win.ll).
# Out-of-band like Http.*: the transport under a game's own netcode.
@ -532,6 +543,14 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "ip") { bare = "udp_ip"; push(labels, "text") }
if (meth == "ip_text") { bare = "udp_ip_text"; push(labels, "ip") }
}
# Process.* — child processes, started and polled (runtime/native/process.ludic +
# process.ll / process_win.ll). Out-of-band, like Http.*.
if (ns == "Process") {
if (meth == "spawn") { bare = "process_spawn"; push(labels, "path"); push(labels, "args") }
if (meth == "poll") { bare = "process_poll"; push(labels, "handle") }
if (meth == "kill") { bare = "process_kill"; push(labels, "handle") }
if (meth == "free") { bare = "process_free"; push(labels, "handle") }
}
# Phase 3: the bare reflection / networking / process builtins, namespaced.
# Each is a pure alias — the callee is rewritten to the bare name below.
if (ns == "World") {

View file

@ -120,6 +120,8 @@ function emit_pak_prelude() -> void {
emith("declare void @splash_show(ptr, i32, i32)\n")
emith("declare void @splash_hide()\n")
emith("declare void @app_set_icon(ptr, i32)\n")
emith("declare void @app_window_hide()\n")
emith("declare void @app_window_show()\n")
}
emit_pak_mount()

View file

@ -254,6 +254,8 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
# Udp.* — any Udp method splices the datagram runtime (and links udp.ll / udp_win.ll).
if e.a.kind == E_ID and e.a.s == "Udp" { g_uses_udp = true }
# Process.* — any Process method splices the child-process runtime (process.ll / process_win.ll).
if e.a.kind == E_ID and e.a.s == "Process" { g_uses_process = true }
# Gl.* — any Gl method splices the OpenGL runtime (and links the GL backend).
if e.a.kind == E_ID and e.a.s == "Gl" { g_uses_gl = true }
# Vk.* — any Vk method splices the Vulkan runtime (and links its loader)
@ -680,6 +682,7 @@ var g_uses_fx: bool = false # Fx.sparks/number/clear -> splice fx.ludic; f
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
var g_uses_udp: bool = false # Udp.* -> splice udp.ludic; links udp.ll (macOS) / udp_win.ll + ws2_32
var g_uses_process: bool = false # Process.* -> splice process.ludic; links process.ll (macOS) / process_win.ll
var g_uses_gl: bool = false # Gl.* -> splice gl.ludic (+ generated gl_api.ludic); links gl.ll + gl_thunks.ll + OpenGL
var g_uses_vk: bool = false # Vk.* -> splice vk.ludic (+ generated vk_api.ludic); links vk_thunks.ll + the platform loader
# issue #64: functions marked @System(Phase) in a prebuilt binary module — the
@ -1197,6 +1200,13 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/udp.ludic")
cur_dir = saved
}
# Process.*: splice the child-process library. Spawning is native (process.ll /
# process_win.ll); the argv array and the Windows command line are built in Ludic.
if g_uses_process {
cur_dir = ""
do_import("runtime/native/process.ludic")
cur_dir = saved
}
# Gl.*: splice the OpenGL surface (gl.ludic + the generated gl_api.ludic). The
# native calls are the linked GL entry points themselves; the window attach is
# is_windowed()-guarded, so a headless build renders into an offscreen context.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -279,6 +279,12 @@ entry {
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/udp_win.ll")} -lws2_32` }
else { cmd = `{cmd} {join_path(home, "runtime/native/udp.ll")}` }
}
# Process.* links the child-process layer: posix_spawn from libSystem on macOS,
# CreateProcessW from kernel32 on Windows.
if g_uses_process {
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/process_win.ll")}` }
else { cmd = `{cmd} {join_path(home, "runtime/native/process.ll")}` }
}
if g_uses_http {
if g_target_win {
let httpw = join_path(home, "runtime/native/http_win.ll")