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

@ -147,6 +147,7 @@ declare i32 @CGWarpMouseCursorPosition(%NSPoint)
@W_scale = internal global i32 3
@W_key = internal global i32 0
@W_running = internal global i32 1
@W_apphidden = internal global i32 0 ; App.window_hide: an ordered-out window is not a closed one
; #50 device layer — a 256-bit held-key set (8 i32) tracked from keyDown/keyUp,
; and the mouse state (position, button mask, per-frame wheel delta).
@W_held = internal global [8 x i32] zeroinitializer
@ -565,6 +566,10 @@ finish:
%hw = icmp ne ptr %win, null
br i1 %hw, label %checkvis, label %done
checkvis:
%ah = load i32, ptr @W_apphidden
%byapp = icmp ne i32 %ah, 0
br i1 %byapp, label %done, label %checkvis2
checkvis2:
%sel_vis = call ptr @sel_registerName(ptr @.s_visib)
%vis = call i8 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_vis)
%gone = icmp eq i8 %vis, 0
@ -1695,6 +1700,47 @@ out:
ret void
}
; ---- App.window_hide / App.window_show ------------------------------------------------
; The game's window leaves the screen and comes back with its view, layer and GL context
; untouched: a launcher steps aside while the game it started runs. The run does not end
; while the window is hidden this way (win_poll's closed-window check skips it). Both are
; no-ops before there is a window.
define void @app_window_hide() {
entry:
%win = load ptr, ptr @W_win
%none = icmp eq ptr %win, null
br i1 %none, label %out, label %go
go:
store i32 1, ptr @W_apphidden
%sel_oo = call ptr @sel_registerName(ptr @.s_oout)
%r = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_oo, ptr null)
br label %out
out:
ret void
}
define void @app_window_show() {
entry:
%win = load ptr, ptr @W_win
%none = icmp eq ptr %win, null
br i1 %none, label %out, label %go
go:
%sel_mk = call ptr @sel_registerName(ptr @.s_mkey)
%r = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_mk, ptr null)
%app = load ptr, ptr @W_app
%noapp = icmp eq ptr %app, null
br i1 %noapp, label %shown, label %activate
activate:
%sel_act = call ptr @sel_registerName(ptr @.s_act)
%a = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %app, ptr %sel_act, i8 1)
br label %shown
shown:
store i32 0, ptr @W_apphidden
br label %out
out:
ret void
}
; ---- the native window, for a graphics API that makes its own surface ------------------
; win32.ll's contract: on macOS the view the game draws in (a Metal-backed layer would hang
; off it); there is no module instance to hand over, so that one is null.

View file

@ -21,6 +21,15 @@
; http_done(slot) -> bool http_status(slot) -> int
; http_body(slot) -> ptr http_blen(slot) -> int
; http_header(slot, name) -> cstr http_free(slot)
;
; Streaming (Http.save_to), which a synchronous NSURLConnection cannot do:
; hs_save_to(slot, path) hs_received(slot) -> int
; hs_expected(slot) -> int hs_cancel(slot)
; A slot given a path before hs_send is sent through one shared NSURLSession whose
; delegate - a class built at run time, like cocoa.ll's view - appends each chunk to the
; file as it arrives (URLSession:dataTask:didReceiveData:) and publishes the status and the
; done flag when the task ends (URLSession:task:didCompleteWithError:). The callbacks run
; on the session's own serial queue; the byte counters are atomics the poller reads.
; ============================================================================
declare ptr @objc_getClass(ptr)
@ -31,6 +40,15 @@ declare void @free(ptr)
declare ptr @memcpy(ptr, ptr, i64)
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
declare i32 @pthread_detach(ptr)
declare ptr @strdup(ptr)
declare ptr @fopen(ptr, ptr)
declare i64 @fwrite(ptr, i64, i64, ptr)
declare i32 @fclose(ptr)
declare ptr @objc_allocateClassPair(ptr, ptr, i64)
declare void @objc_registerClassPair(ptr)
declare i8 @class_addMethod(ptr, ptr, ptr, ptr)
declare ptr @objc_autoreleasePoolPush()
declare void @objc_autoreleasePoolPop(ptr)
@.h_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00"
@.h_nsstr = private unnamed_addr constant [9 x i8] c"NSString\00"
@ -59,6 +77,26 @@ declare i32 @pthread_detach(ptr)
@.h_rel = private unnamed_addr constant [8 x i8] c"release\00"
@.h_empty = private unnamed_addr constant [1 x i8] c"\00"
@.h_nsobj = private unnamed_addr constant [9 x i8] c"NSObject\00"
@.h_dlgcls = private unnamed_addr constant [18 x i8] c"LudicHttpDelegate\00"
@.h_sesscl = private unnamed_addr constant [13 x i8] c"NSURLSession\00"
@.h_cfgcl = private unnamed_addr constant [26 x i8] c"NSURLSessionConfiguration\00"
@.h_defcfg = private unnamed_addr constant [28 x i8] c"defaultSessionConfiguration\00"
@.h_swc = private unnamed_addr constant [49 x i8] c"sessionWithConfiguration:delegate:delegateQueue:\00"
@.h_dtwr = private unnamed_addr constant [21 x i8] c"dataTaskWithRequest:\00"
@.h_resume = private unnamed_addr constant [7 x i8] c"resume\00"
@.h_cancel = private unnamed_addr constant [7 x i8] c"cancel\00"
@.h_respg = private unnamed_addr constant [9 x i8] c"response\00"
@.h_cbexp = private unnamed_addr constant [30 x i8] c"countOfBytesExpectedToReceive\00"
@.h_ecl = private unnamed_addr constant [22 x i8] c"expectedContentLength\00"
@.h_seldat = private unnamed_addr constant [36 x i8] c"URLSession:dataTask:didReceiveData:\00"
@.h_seldon = private unnamed_addr constant [38 x i8] c"URLSession:task:didCompleteWithError:\00"
@.h_enc5 = private unnamed_addr constant [7 x i8] c"v@:@@@\00"
@.h_rts = private unnamed_addr constant [20 x i8] c"respondsToSelector:\00"
@.h_wb = private unnamed_addr constant [3 x i8] c"wb\00"
@.h_aenc = private unnamed_addr constant [16 x i8] c"Accept-Encoding\00"
@.h_ident = private unnamed_addr constant [9 x i8] c"identity\00"
; HTTP_SLOTS = 8
@H_req = internal global [8 x ptr] zeroinitializer
@H_resp = internal global [8 x ptr] zeroinitializer
@ -66,6 +104,15 @@ declare i32 @pthread_detach(ptr)
@H_blen = internal global [8 x i32] zeroinitializer
@H_status = internal global [8 x i32] zeroinitializer
@H_done = internal global [8 x i32] zeroinitializer
; streaming (Http.save_to)
@H_path = internal global [8 x ptr] zeroinitializer ; strdup'd file path, set before send
@H_file = internal global [8 x ptr] zeroinitializer ; FILE* while the task runs
@H_task = internal global [8 x ptr] zeroinitializer ; the retained NSURLSessionDataTask
@H_recv = internal global [8 x i32] zeroinitializer ; body bytes written (atomic)
@H_exp = internal global [8 x i32] zeroinitializer ; Content-Length or -1 (atomic)
@H_werr = internal global [8 x i32] zeroinitializer ; a write to the file failed
@H_strm = internal global [8 x i32] zeroinitializer ; this slot was sent streaming
@H_sess = internal global ptr null ; the shared streaming session
; --- request construction ---------------------------------------------------
@ -218,6 +265,14 @@ entry:
store ptr null, ptr %respp
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
store ptr %req, ptr %reqp
%pathp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%path = load ptr, ptr %pathp
%strm = icmp ne ptr %path, null
br i1 %strm, label %stream, label %thread
stream:
call void @hs_stream_start(i32 %slot, ptr %req, ptr %path)
ret void
thread:
%tid = alloca i64
%arg = inttoptr i32 %slot to ptr
%rc = call i32 @pthread_create(ptr %tid, ptr null, ptr @hs_worker, ptr %arg)
@ -265,10 +320,16 @@ entry:
%nores = icmp eq ptr %resp, null
br i1 %nores, label %empty, label %ask
ask:
; only an HTTP response has headers (a file:// one does not)
%sel_vh = call ptr @sel_registerName(ptr @.h_vhdr)
%sel_rts = call ptr @sel_registerName(ptr @.h_rts)
%has = call i8 (ptr, ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_rts, ptr %sel_vh)
%nohdr = icmp eq i8 %has, 0
br i1 %nohdr, label %empty, label %lookup
lookup:
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
%ns = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
%sel_vh = call ptr @sel_registerName(ptr @.h_vhdr)
%val = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_vh, ptr %ns)
%noval = icmp eq ptr %val, null
br i1 %noval, label %empty, label %tocstr
@ -302,5 +363,346 @@ rr:
store ptr null, ptr %rp
br label %out
out:
call void @hs_stream_free(i32 %slot)
ret void
}
; --- streaming (Http.save_to) -------------------------------------------------
define void @hs_save_to(i32 %slot, ptr %path) {
entry:
%pp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%old = load ptr, ptr %pp
%had = icmp ne ptr %old, null
br i1 %had, label %drop, label %set
drop:
call void @free(ptr %old)
br label %set
set:
%cp = call ptr @strdup(ptr %path)
store ptr %cp, ptr %pp
ret void
}
define i32 @hs_received(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
define i32 @hs_expected(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
; Stop a running streaming task; its completion callback then publishes status 0. An
; in-memory request cannot be interrupted and simply runs to its end.
define void @hs_cancel(i32 %slot) {
entry:
%sp = getelementptr [8 x i32], ptr @H_strm, i32 0, i32 %slot
%s = load i32, ptr %sp
%is = icmp ne i32 %s, 0
br i1 %is, label %task, label %out
task:
%tp = getelementptr [8 x ptr], ptr @H_task, i32 0, i32 %slot
%t = load ptr, ptr %tp
%not = icmp eq ptr %t, null
br i1 %not, label %out, label %go
go:
%sel = call ptr @sel_registerName(ptr @.h_cancel)
%r = call ptr (ptr, ptr) @objc_msgSend(ptr %t, ptr %sel)
br label %out
out:
ret void
}
define internal void @hs_stream_free(i32 %slot) {
entry:
%tp = getelementptr [8 x ptr], ptr @H_task, i32 0, i32 %slot
%t = load ptr, ptr %tp
%not = icmp eq ptr %t, null
br i1 %not, label %file, label %rel
rel:
%sel = call ptr @sel_registerName(ptr @.h_rel)
%r = call ptr (ptr, ptr) @objc_msgSend(ptr %t, ptr %sel)
store ptr null, ptr %tp
br label %file
file:
%fp = getelementptr [8 x ptr], ptr @H_file, i32 0, i32 %slot
%f = load ptr, ptr %fp
%nof = icmp eq ptr %f, null
br i1 %nof, label %path, label %close
close:
%c = call i32 @fclose(ptr %f)
store ptr null, ptr %fp
br label %path
path:
%pp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%p = load ptr, ptr %pp
%nop = icmp eq ptr %p, null
br i1 %nop, label %out, label %freep
freep:
call void @free(ptr %p)
store ptr null, ptr %pp
br label %out
out:
%sp = getelementptr [8 x i32], ptr @H_strm, i32 0, i32 %slot
store i32 0, ptr %sp
ret void
}
; the slot whose task this is, or -1
define internal i32 @hs_slot_of(ptr %task) {
entry:
br label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %i1, %next ]
%tp = getelementptr [8 x ptr], ptr @H_task, i32 0, i32 %i
%t = load ptr, ptr %tp
%hit = icmp eq ptr %t, %task
br i1 %hit, label %found, label %next
next:
%i1 = add i32 %i, 1
%more = icmp slt i32 %i1, 8
br i1 %more, label %loop, label %none
found:
ret i32 %i
none:
ret i32 -1
}
; -URLSession:dataTask:didReceiveData: - append the chunk, count it, note the length
define internal void @hs_dlg_data(ptr %self, ptr %cmd, ptr %session, ptr %task, ptr %data) {
entry:
%slot = call i32 @hs_slot_of(ptr %task)
%bad = icmp slt i32 %slot, 0
br i1 %bad, label %out, label %file
file:
%fp = getelementptr [8 x ptr], ptr @H_file, i32 0, i32 %slot
%f = load ptr, ptr %fp
%nof = icmp eq ptr %f, null
br i1 %nof, label %out, label %write
write:
%sel_len = call ptr @sel_registerName(ptr @.h_length)
%n = call i64 (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_len)
%sel_by = call ptr @sel_registerName(ptr @.h_bytes)
%b = call ptr (ptr, ptr) @objc_msgSend(ptr %data, ptr %sel_by)
%w = call i64 @fwrite(ptr %b, i64 1, i64 %n, ptr %f)
%short = icmp ne i64 %w, %n
br i1 %short, label %failed, label %count
failed:
%ep = getelementptr [8 x i32], ptr @H_werr, i32 0, i32 %slot
store i32 1, ptr %ep
%sel_c = call ptr @sel_registerName(ptr @.h_cancel)
%rc = call ptr (ptr, ptr) @objc_msgSend(ptr %task, ptr %sel_c)
br label %count
count:
%rp = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
%r0 = load atomic i32, ptr %rp acquire, align 4
%w32 = trunc i64 %w to i32
%r1 = add i32 %r0, %w32
store atomic i32 %r1, ptr %rp release, align 4
%sel_x = call ptr @sel_registerName(ptr @.h_cbexp)
%x = call i64 (ptr, ptr) @objc_msgSend(ptr %task, ptr %sel_x)
%x32 = trunc i64 %x to i32
%neg = icmp slt i64 %x, 0
%xv = select i1 %neg, i32 -1, i32 %x32
%xp = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
store atomic i32 %xv, ptr %xp release, align 4
br label %out
out:
ret void
}
; -URLSession:task:didCompleteWithError: - close the file, publish status and done
define internal void @hs_dlg_done(ptr %self, ptr %cmd, ptr %session, ptr %task, ptr %error) {
entry:
%slot = call i32 @hs_slot_of(ptr %task)
%bad = icmp slt i32 %slot, 0
br i1 %bad, label %out, label %close
close:
%fp = getelementptr [8 x ptr], ptr @H_file, i32 0, i32 %slot
%f = load ptr, ptr %fp
%nof = icmp eq ptr %f, null
br i1 %nof, label %judge, label %doclose
doclose:
%cr = call i32 @fclose(ptr %f)
store ptr null, ptr %fp
%cfail = icmp ne i32 %cr, 0
br i1 %cfail, label %markw, label %judge
markw:
%ep0 = getelementptr [8 x i32], ptr @H_werr, i32 0, i32 %slot
store i32 1, ptr %ep0
br label %judge
judge:
%ep = getelementptr [8 x i32], ptr @H_werr, i32 0, i32 %slot
%we = load i32, ptr %ep
%werr = icmp ne i32 %we, 0
%neterr = icmp ne ptr %error, null
%failed = or i1 %werr, %neterr
br i1 %failed, label %publish, label %resp
resp:
%sel_r = call ptr @sel_registerName(ptr @.h_respg)
%rs = call ptr (ptr, ptr) @objc_msgSend(ptr %task, ptr %sel_r)
%nors = icmp eq ptr %rs, null
br i1 %nors, label %publish, label %keep
keep:
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %rs, ptr %sel_ret)
%respp = getelementptr [8 x ptr], ptr @H_resp, i32 0, i32 %slot
store ptr %rs, ptr %respp
; a file:// URL answers with a plain NSURLResponse, which has no status code: one that
; completed without an error is reported as 200
%sel_st = call ptr @sel_registerName(ptr @.h_status)
%sel_rts = call ptr @sel_registerName(ptr @.h_rts)
%ishttp = call i8 (ptr, ptr, ptr) @objc_msgSend(ptr %rs, ptr %sel_rts, ptr %sel_st)
%nothttp = icmp eq i8 %ishttp, 0
br i1 %nothttp, label %plain, label %httpst
plain:
br label %setst
httpst:
%st64 = call i64 (ptr, ptr) @objc_msgSend(ptr %rs, ptr %sel_st)
%st0 = trunc i64 %st64 to i32
br label %setst
setst:
%st = phi i32 [ 200, %plain ], [ %st0, %httpst ]
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
store i32 %st, ptr %stp
; an empty body never reached didReceiveData: take the length from the response
%xp = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
%x0 = load atomic i32, ptr %xp acquire, align 4
%unk = icmp slt i32 %x0, 0
br i1 %unk, label %fromresp, label %publish
fromresp:
%sel_e = call ptr @sel_registerName(ptr @.h_ecl)
%e64 = call i64 (ptr, ptr) @objc_msgSend(ptr %rs, ptr %sel_e)
%e32 = trunc i64 %e64 to i32
%eneg = icmp slt i64 %e64, 0
%ev = select i1 %eneg, i32 -1, i32 %e32
store atomic i32 %ev, ptr %xp release, align 4
br label %publish
publish:
%rp = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
%got = load atomic i32, ptr %rp acquire, align 4
%blenp = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
store i32 %got, ptr %blenp
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
store atomic i32 1, ptr %donep release, align 4
br label %out
out:
ret void
}
; The one session every streaming request goes through, made on first use (hs_send runs on
; the game thread only). Its delegate is an instance of a class built here.
define internal ptr @hs_session() {
entry:
%have = load ptr, ptr @H_sess
%got = icmp ne ptr %have, null
br i1 %got, label %ready, label %make
ready:
ret ptr %have
make:
%cls0 = call ptr @objc_getClass(ptr @.h_dlgcls)
%exists = icmp ne ptr %cls0, null
br i1 %exists, label %inst, label %build
build:
%super = call ptr @objc_getClass(ptr @.h_nsobj)
%nc = call ptr @objc_allocateClassPair(ptr %super, ptr @.h_dlgcls, i64 0)
%sd = call ptr @sel_registerName(ptr @.h_seldat)
%m1 = call i8 @class_addMethod(ptr %nc, ptr %sd, ptr @hs_dlg_data, ptr @.h_enc5)
%sc = call ptr @sel_registerName(ptr @.h_seldon)
%m2 = call i8 @class_addMethod(ptr %nc, ptr %sc, ptr @hs_dlg_done, ptr @.h_enc5)
call void @objc_registerClassPair(ptr %nc)
br label %inst
inst:
%cls = phi ptr [ %cls0, %make ], [ %nc, %build ]
%sel_a = call ptr @sel_registerName(ptr @.h_alloc)
%d0 = call ptr (ptr, ptr) @objc_msgSend(ptr %cls, ptr %sel_a)
%sel_i = call ptr @sel_registerName(ptr @.h_init)
%dlg = call ptr (ptr, ptr) @objc_msgSend(ptr %d0, ptr %sel_i)
%cfgcls = call ptr @objc_getClass(ptr @.h_cfgcl)
%sel_dc = call ptr @sel_registerName(ptr @.h_defcfg)
%cfg = call ptr (ptr, ptr) @objc_msgSend(ptr %cfgcls, ptr %sel_dc)
%sesscls = call ptr @objc_getClass(ptr @.h_sesscl)
%sel_sw = call ptr @sel_registerName(ptr @.h_swc)
%sess = call ptr (ptr, ptr, ptr, ptr, ptr) @objc_msgSend(ptr %sesscls, ptr %sel_sw, ptr %cfg, ptr %dlg, ptr null)
%none = icmp eq ptr %sess, null
br i1 %none, label %fail, label %hold
hold:
; the session retains its delegate; it is kept for the life of the program
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
%rs = call ptr (ptr, ptr) @objc_msgSend(ptr %sess, ptr %sel_ret)
store ptr %sess, ptr @H_sess
ret ptr %sess
fail:
ret ptr null
}
; Send `req` on `slot` as a streaming download into `path`. The file is created first; if
; it cannot be, or the task cannot be made, the request finishes at once with status 0.
define internal void @hs_stream_start(i32 %slot, ptr %req, ptr %path) {
entry:
%pool = call ptr @objc_autoreleasePoolPush()
%sp = getelementptr [8 x i32], ptr @H_strm, i32 0, i32 %slot
store i32 1, ptr %sp
%rp = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
store atomic i32 0, ptr %rp release, align 4
%xp = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
store atomic i32 -1, ptr %xp release, align 4
%ep = getelementptr [8 x i32], ptr @H_werr, i32 0, i32 %slot
store i32 0, ptr %ep
%sel_rel = call ptr @sel_registerName(ptr @.h_rel)
%f = call ptr @fopen(ptr %path, ptr @.h_wb)
%nof = icmp eq ptr %f, null
br i1 %nof, label %finish, label %opened
opened:
%fp = getelementptr [8 x ptr], ptr @H_file, i32 0, i32 %slot
store ptr %f, ptr %fp
; the bytes on disk are the bytes the counter reports: ask for no transfer compression
; unless the caller chose an encoding
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
%aen = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr @.h_aenc)
%sel_vh = call ptr @sel_registerName(ptr @.h_vhdr)
%cur = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_vh, ptr %aen)
%unset = icmp eq ptr %cur, null
br i1 %unset, label %setenc, label %session
setenc:
%idn = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr @.h_ident)
%sel_sh = call ptr @sel_registerName(ptr @.h_shdr)
%r0 = call ptr (ptr, ptr, ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_sh, ptr %idn, ptr %aen)
br label %session
session:
%sess = call ptr @hs_session()
%nosess = icmp eq ptr %sess, null
br i1 %nosess, label %closefile, label %mktask
mktask:
%sel_dt = call ptr @sel_registerName(ptr @.h_dtwr)
%task = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %sess, ptr %sel_dt, ptr %req)
%notask = icmp eq ptr %task, null
br i1 %notask, label %closefile, label %run
run:
%sel_ret = call ptr @sel_registerName(ptr @.h_retain)
%rt = call ptr (ptr, ptr) @objc_msgSend(ptr %task, ptr %sel_ret)
%tp = getelementptr [8 x ptr], ptr @H_task, i32 0, i32 %slot
store ptr %task, ptr %tp
%rr = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_rel)
%sel_go = call ptr @sel_registerName(ptr @.h_resume)
%g = call ptr (ptr, ptr) @objc_msgSend(ptr %task, ptr %sel_go)
call void @objc_autoreleasePoolPop(ptr %pool)
ret void
closefile:
%c = call i32 @fclose(ptr %f)
store ptr null, ptr %fp
br label %finish
finish:
%rr2 = call ptr (ptr, ptr) @objc_msgSend(ptr %req, ptr %sel_rel)
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
store atomic i32 1, ptr %donep release, align 4
call void @objc_autoreleasePoolPop(ptr %pool)
ret void
}

View file

@ -18,6 +18,14 @@
# any target and is what the test suite exercises without touching the network.
# ============================================================================
# Streaming downloads (Http.save_to): the body goes to a file as it arrives, and
# Http.received / Http.expected report progress while the request is pending. These
# natives live beside the hs_* transport in http.ll / http_win.ll.
extern function hs_save_to(slot: int, path: pointer) -> void = "hs_save_to"
extern function hs_received(slot: int) -> int = "hs_received"
extern function hs_expected(slot: int) -> int = "hs_expected"
extern function hs_cancel(slot: int) -> void = "hs_cancel"
const HTTP_SLOTS: int = 8
var h_ready: bool = false
@ -30,6 +38,7 @@ var h_blen: words = null # body length
var h_req: pointers = null # the pending NSURLRequest (native, before send)
var h_body: pointers = null # body bytes (NUL-terminated); owned by the slot
var h_hdr: pointers = null # raw header block (parsed handles only); owned by the slot
var h_save: words = null # 1 = the body streams to a file (Http.save_to)
function http_init() -> void {
if h_ready { return }
@ -42,11 +51,13 @@ function http_init() -> void {
h_req = bytes(HTTP_SLOTS * 8)
h_body = bytes(HTTP_SLOTS * 8)
h_hdr = bytes(HTTP_SLOTS * 8)
h_save = words(HTTP_SLOTS)
var i = 0
while i < HTTP_SLOTS {
h_used[i] = 0; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
h_status[i] = 0; h_blen[i] = 0
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
h_save[i] = 0
i += 1
}
h_ready = true
@ -60,6 +71,7 @@ function http_slot_alloc() -> int {
h_used[i] = 1; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
h_status[i] = 0; h_blen[i] = 0
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
h_save[i] = 0
return i
}
i += 1
@ -70,7 +82,22 @@ function http_slot_alloc() -> int {
function http_valid(h: int) -> bool {
http_init()
if (h < 1) or (h > HTTP_SLOTS) { return false }
return h_used[h - 1] != 0
return h_used[h - 1] == 1
}
# A native request freed while its worker was still running is parked (h_used 2) rather
# than released, so the worker never writes into a slot a new request has taken. Once the
# worker has finished, the slot's native resources are freed and it is free again.
function http_reap() -> void {
http_init()
var i = 0
while i < HTTP_SLOTS {
if (h_used[i] == 2) and (hs_done(i) != 0) {
hs_free(i)
h_used[i] = 0
}
i += 1
}
}
# ---- length of a NUL-terminated C string -----------------------------------
@ -84,6 +111,7 @@ function http_cstr_len(p: pointer) -> int {
# ---- request construction (native) -----------------------------------------
# Open a request without sending it, so headers / a body can be added first.
function http_open(method: pointer, url: pointer) -> int {
http_reap()
let req = hs_req_new(method, url)
if req == null { return 0 }
let s = http_slot_alloc()
@ -110,6 +138,45 @@ function http_body(h: int, body: pointer) -> void {
http_body_n(h, body, http_cstr_len(body))
}
# Stream the response body to the file at `path` instead of keeping it in memory. Must be
# called before Http.send. The file is created (or truncated) when the request is sent,
# and written directly - no temporary name - so a failed or cancelled download leaves
# whatever arrived there; the caller picks the name and renames it once the status is
# good. Http.text of such a request is empty (null) and Http.body_len is the bytes written.
function http_save_to(h: int, path: pointer) -> void {
if not http_valid(h) { return }
let s = h - 1
if (h_native[s] == 1) and (h_sent[s] == 0) and (path != null) {
hs_save_to(s, path)
h_save[s] = 1
}
}
# Body bytes received so far. Live while a Http.save_to request is pending; for an
# in-memory request it is 0 until the reply lands, then the body length.
function http_received(h: int) -> int {
if not http_valid(h) { return 0 }
let s = h - 1
if h_native[s] == 0 { return h_blen[s] }
if h_sent[s] == 0 { return 0 }
if h_save[s] == 1 { return hs_received(s) }
if h_res[s] == 1 { return h_blen[s] }
return 0
}
# The body's announced length (Content-Length), or -1 while it is not known. For a
# Http.save_to request it is known as soon as the headers arrive; for an in-memory request
# it is -1 until the reply lands, then the body length.
function http_expected(h: int) -> int {
if not http_valid(h) { return -1 }
let s = h - 1
if h_native[s] == 0 { return h_blen[s] }
if h_sent[s] == 0 { return -1 }
if h_save[s] == 1 { return hs_expected(s) }
if h_res[s] == 1 { return h_blen[s] }
return -1
}
# Dispatch the request onto the background worker.
function http_send_req(h: int) -> void {
if not http_valid(h) { return }
@ -188,7 +255,16 @@ function http_header_of(h: int, name: pointer) -> pointer {
function http_close(h: int) -> void {
if not http_valid(h) { return }
let s = h - 1
if h_native[s] == 1 { hs_free(s) }
if h_native[s] == 1 {
if (h_sent[s] == 1) and (h_res[s] == 0) and (hs_done(s) == 0) {
# still running: stop it, and park the slot until the worker has let go
hs_cancel(s)
h_used[s] = 2
h_req[s] = null; h_body[s] = null; h_hdr[s] = null
return
}
hs_free(s)
}
else {
if h_body[s] != null { free(h_body[s]) }
if h_hdr[s] != null { free(h_hdr[s]) }

View file

@ -18,6 +18,11 @@
; hs_body(slot) -> ptr hs_blen(slot) -> int
; hs_header(slot, name) -> cstr hs_free(slot)
;
; Streaming (Http.save_to): hs_save_to(slot, path) before hs_send makes the worker create
; the file first and write each chunk into it as it is read, counting the bytes in an
; atomic the poller reads (hs_received); hs_expected is the Content-Length, or -1.
; hs_cancel asks the worker to stop between chunks.
;
; WinHTTP speaks UTF-16; every string crosses in UTF-8 and is converted at the call.
; A request record: method 0, url 8, header lines 16 ("Name: value\r\n"...), body 24,
; body length 32.
@ -44,6 +49,8 @@ declare i32 @WinHttpQueryHeaders(ptr, i32, ptr, ptr, ptr, ptr)
declare i32 @WinHttpQueryDataAvailable(ptr, ptr)
declare i32 @WinHttpReadData(ptr, ptr, i32, ptr)
declare i32 @WinHttpCloseHandle(ptr)
declare ptr @CreateFileW(ptr, i32, i32, ptr, i32, i32, ptr)
declare i32 @WriteFile(ptr, ptr, i32, ptr, ptr)
@.h_agent = private unnamed_addr constant [6 x i16] [i16 76, i16 117, i16 100, i16 105, i16 99, i16 0] ; L"Ludic"
@.h_empty = private unnamed_addr constant [1 x i8] c"\00"
@ -60,6 +67,12 @@ declare i32 @WinHttpCloseHandle(ptr)
@H_status = internal global [8 x i32] zeroinitializer
@H_done = internal global [8 x i32] zeroinitializer
@H_hdr = internal global [8 x ptr] zeroinitializer
; streaming (Http.save_to)
@H_path = internal global [8 x ptr] zeroinitializer ; UTF-8 file path, set before send
@H_fileh = internal global [8 x ptr] zeroinitializer ; the file's HANDLE while the worker writes
@H_recv = internal global [8 x i32] zeroinitializer ; body bytes written (atomic)
@H_exp = internal global [8 x i32] zeroinitializer ; Content-Length or -1 (atomic)
@H_cancel = internal global [8 x i32] zeroinitializer ; stop between chunks (atomic)
; ---- strings -----------------------------------------------------------------
@ -220,6 +233,23 @@ entry:
%murl = load ptr, ptr %murlp
%wurl = call ptr @h_wide(ptr %murl)
store ptr %wurl, ptr %wurlp
; a streaming request creates (truncates) its file before anything goes on the wire
%spathp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%spath = load ptr, ptr %spathp
%strm = icmp ne ptr %spath, null
br i1 %strm, label %mkfile, label %opensess
mkfile:
%wpath = call ptr @h_wide(ptr %spath)
; GENERIC_WRITE, FILE_SHARE_READ, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL
%fh = call ptr @CreateFileW(ptr %wpath, i32 1073741824, i32 1, ptr null, i32 2, i32 128, ptr null)
call void @free(ptr %wpath)
%badfh = icmp eq ptr %fh, inttoptr (i64 -1 to ptr)
br i1 %badfh, label %done, label %keepfh
keepfh:
%fhp = getelementptr [8 x ptr], ptr @H_fileh, i32 0, i32 %slot
store ptr %fh, ptr %fhp
br label %opensess
opensess:
; WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY: the system's proxy settings, as NSURLConnection uses
%sess = call ptr @WinHttpOpen(ptr @.h_agent, i32 4, ptr null, ptr null, i32 0)
%sessp = getelementptr [8 x ptr], ptr @H_sess, i32 0, i32 %slot
@ -307,6 +337,71 @@ status:
%st = load i32, ptr %num
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
store i32 %st, ptr %stp
br i1 %strm, label %stream, label %inmem
stream:
; WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER
store i32 -1, ptr %num
store i32 4, ptr %numlen
%qcl = call i32 @WinHttpQueryHeaders(ptr %hreq, i32 536870917, ptr null, ptr %num, ptr %numlen, ptr null)
%cl = load i32, ptr %num
%noclq = icmp eq i32 %qcl, 0
%clv = select i1 %noclq, i32 -1, i32 %cl
%xp = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
store atomic i32 %clv, ptr %xp release, align 4
%sbuf = call ptr @malloc(i64 65536)
store ptr %sbuf, ptr %bufp
%sfhp = getelementptr [8 x ptr], ptr @H_fileh, i32 0, i32 %slot
%sfh = load ptr, ptr %sfhp
%rcp = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
%canp = getelementptr [8 x i32], ptr @H_cancel, i32 0, i32 %slot
br label %sloop
sloop:
%cn = load atomic i32, ptr %canp acquire, align 4
%cancelled = icmp ne i32 %cn, 0
br i1 %cancelled, label %sfail, label %savail
savail:
store i32 0, ptr %avail
%sqa = call i32 @WinHttpQueryDataAvailable(ptr %hreq, ptr %avail)
%sav = load i32, ptr %avail
%sqfail = icmp eq i32 %sqa, 0
br i1 %sqfail, label %sfail, label %sany
sany:
%snone = icmp eq i32 %sav, 0
br i1 %snone, label %sdone, label %sread
sread:
%sbig = icmp ugt i32 %sav, 65536
%sn = select i1 %sbig, i32 65536, i32 %sav
store i32 0, ptr %got
%srd = call i32 @WinHttpReadData(ptr %hreq, ptr %sbuf, i32 %sn, ptr %got)
%sg = load i32, ptr %got
%srfail = icmp eq i32 %srd, 0
br i1 %srfail, label %sfail, label %sgot
sgot:
%sg0 = icmp eq i32 %sg, 0
br i1 %sg0, label %sdone, label %swrite
swrite:
store i32 0, ptr %num
%wr = call i32 @WriteFile(ptr %sfh, ptr %sbuf, i32 %sg, ptr %num, ptr null)
%wn = load i32, ptr %num
%wfail = icmp eq i32 %wr, 0
%wshort = icmp ne i32 %wn, %sg
%wbad = or i1 %wfail, %wshort
br i1 %wbad, label %sfail, label %scount
scount:
%r0 = load atomic i32, ptr %rcp acquire, align 4
%r1 = add i32 %r0, %sg
store atomic i32 %r1, ptr %rcp release, align 4
br label %sloop
sfail:
store i32 0, ptr %stp
br label %sdone
sdone:
call void @free(ptr %sbuf)
%total = load atomic i32, ptr %rcp acquire, align 4
%sblenslot = getelementptr [8 x i32], ptr @H_blen, i32 0, i32 %slot
store i32 %total, ptr %sblenslot
br label %done
inmem:
%b0 = call ptr @malloc(i64 4097)
store ptr %b0, ptr %bufp
store i64 0, ptr %lenp
@ -369,6 +464,17 @@ done:
call void @h_free_if(ptr %f3)
%f4 = load ptr, ptr %whdrp
call void @h_free_if(ptr %f4)
; a streaming request's file is closed before done is published: done means the bytes
; are on disk
%dfhp = getelementptr [8 x ptr], ptr @H_fileh, i32 0, i32 %slot
%dfh = load ptr, ptr %dfhp
%nodfh = icmp eq ptr %dfh, null
br i1 %nodfh, label %publish, label %closefh
closefh:
%cfh = call i32 @CloseHandle(ptr %dfh)
store ptr null, ptr %dfhp
br label %publish
publish:
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
store atomic i32 1, ptr %donep release, align 4
ret i32 0
@ -387,6 +493,12 @@ entry:
store i32 0, ptr %blenp
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
store ptr %req, ptr %reqp
%rcp = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
store atomic i32 0, ptr %rcp release, align 4
%xp = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
store atomic i32 -1, ptr %xp release, align 4
%canp = getelementptr [8 x i32], ptr @H_cancel, i32 0, i32 %slot
store atomic i32 0, ptr %canp release, align 4
%nil = icmp eq ptr %req, null
br i1 %nil, label %nothing, label %start
nothing:
@ -510,6 +622,10 @@ entry:
%ss = load ptr, ptr %ssp
call void @h_close_if(ptr %ss)
store ptr null, ptr %ssp
%pthp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%pth = load ptr, ptr %pthp
call void @h_free_if(ptr %pth)
store ptr null, ptr %pthp
%recp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
%rec = load ptr, ptr %recp
%norec = icmp eq ptr %rec, null
@ -532,3 +648,38 @@ freerec:
out:
ret void
}
; ---- streaming (Http.save_to) ---------------------------------------------------------
define void @hs_save_to(i32 %slot, ptr %path) {
entry:
%pp = getelementptr [8 x ptr], ptr @H_path, i32 0, i32 %slot
%old = load ptr, ptr %pp
call void @h_free_if(ptr %old)
%cp = call ptr @h_dup(ptr %path)
store ptr %cp, ptr %pp
ret void
}
define i32 @hs_received(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_recv, i32 0, i32 %slot
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
define i32 @hs_expected(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_exp, i32 0, i32 %slot
%v = load atomic i32, ptr %p acquire, align 4
ret i32 %v
}
; ask a streaming worker to stop before its next chunk (it then publishes status 0); an
; in-memory request runs to its end
define void @hs_cancel(i32 %slot) {
entry:
%p = getelementptr [8 x i32], ptr @H_cancel, i32 0, i32 %slot
store atomic i32 1, ptr %p release, align 4
ret void
}

187
runtime/native/process.ll Normal file
View file

@ -0,0 +1,187 @@
; ============================================================================
; process.ll — child processes behind Process.* (macOS / POSIX, posix_spawn).
;
; Linked into any program that uses Process.* (selfhost/main.ludic, tools/ludic-cli/build.ludic).
; process_win.ll is the same interface over CreateProcessW.
;
; lu_proc_spawn(path, argv, cmdline) -> int start path with the NULL-terminated argv
; (argv[0] = path) and this process's
; environment and working directory; a handle
; 0..15, or -1. cmdline is Windows' and unused.
; lu_proc_poll(h) -> int -1 while running, else the exit code
; (128 + signal when a signal ended it)
; lu_proc_kill(h) SIGKILL
; lu_proc_free(h) drop the handle; a running child is reaped
; later, by whichever call next looks
;
; A slot is free (0), running (1), finished with its code cached (2), or let go while still
; running (3) - kept only so the child is reaped rather than left a zombie.
; ============================================================================
declare i32 @posix_spawn(ptr, ptr, ptr, ptr, ptr, ptr)
declare ptr @_NSGetEnviron()
declare i32 @waitpid(i32, ptr, i32)
declare i32 @kill(i32, i32)
@P_pid = internal global [16 x i32] zeroinitializer
@P_state = internal global [16 x i32] zeroinitializer
@P_code = internal global [16 x i32] zeroinitializer
define internal i1 @pr_bad(i32 %h) {
entry:
%lo = icmp slt i32 %h, 0
%hi = icmp sgt i32 %h, 15
%bad = or i1 %lo, %hi
ret i1 %bad
}
; Ask once, without waiting, whether slot i's child has ended. For a running slot the code
; is cached (state 2); a let-go slot becomes free. Returns 1 when it has ended.
define internal i32 @pr_reap(i32 %i) {
entry:
%status = alloca i32, align 4
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %i
%st = load i32, ptr %stp
%run = icmp eq i32 %st, 1
%gone = icmp eq i32 %st, 3
%live = or i1 %run, %gone
br i1 %live, label %ask, label %no
ask:
%pidp = getelementptr [16 x i32], ptr @P_pid, i32 0, i32 %i
%pid = load i32, ptr %pidp
store i32 0, ptr %status
%w = call i32 @waitpid(i32 %pid, ptr %status, i32 1) ; WNOHANG
%still = icmp eq i32 %w, 0
br i1 %still, label %no, label %ended
ended:
; waitpid failing (the child is not ours to wait for any more) reads as 255
%err = icmp slt i32 %w, 0
%s = load i32, ptr %status
%sig = and i32 %s, 127
%exited = icmp eq i32 %sig, 0
%hi = lshr i32 %s, 8
%ecode = and i32 %hi, 255
%scode = add i32 %sig, 128
%c0 = select i1 %exited, i32 %ecode, i32 %scode
%code = select i1 %err, i32 255, i32 %c0
%cp = getelementptr [16 x i32], ptr @P_code, i32 0, i32 %i
store i32 %code, ptr %cp
%next = select i1 %run, i32 2, i32 0
store i32 %next, ptr %stp
ret i32 1
no:
ret i32 0
}
define i32 @lu_proc_spawn(ptr %path, ptr %argv, ptr %cmdline) {
entry:
%pidv = alloca i32, align 4
br label %find
find:
%i = phi i32 [ 0, %entry ], [ %i1, %next ]
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %i
%st = load i32, ptr %stp
%isgone = icmp eq i32 %st, 3
br i1 %isgone, label %reap, label %check
reap:
%r = call i32 @pr_reap(i32 %i)
br label %check
check:
%st2 = load i32, ptr %stp
%free = icmp eq i32 %st2, 0
br i1 %free, label %spawn, label %next
next:
%i1 = add i32 %i, 1
%more = icmp slt i32 %i1, 16
br i1 %more, label %find, label %fail
spawn:
%envpp = call ptr @_NSGetEnviron()
%envp = load ptr, ptr %envpp
store i32 0, ptr %pidv
%rc = call i32 @posix_spawn(ptr %pidv, ptr %path, ptr null, ptr null, ptr %argv, ptr %envp)
%ok = icmp eq i32 %rc, 0
br i1 %ok, label %keep, label %fail
keep:
%pid = load i32, ptr %pidv
%pidp = getelementptr [16 x i32], ptr @P_pid, i32 0, i32 %i
store i32 %pid, ptr %pidp
store i32 1, ptr %stp
%cp = getelementptr [16 x i32], ptr @P_code, i32 0, i32 %i
store i32 -1, ptr %cp
ret i32 %i
fail:
ret i32 -1
}
define i32 @lu_proc_poll(i32 %h) {
entry:
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %none, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%run = icmp eq i32 %st, 1
br i1 %run, label %ask, label %known
ask:
%r = call i32 @pr_reap(i32 %h)
%ended = icmp eq i32 %r, 1
br i1 %ended, label %known, label %running
running:
ret i32 -1
known:
%st2 = load i32, ptr %stp
%done = icmp eq i32 %st2, 2
br i1 %done, label %code, label %none
code:
%cp = getelementptr [16 x i32], ptr @P_code, i32 0, i32 %h
%c = load i32, ptr %cp
ret i32 %c
none:
; not a handle: nothing is running under it
ret i32 255
}
define void @lu_proc_kill(i32 %h) {
entry:
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %out, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%run = icmp eq i32 %st, 1
br i1 %run, label %go, label %out
go:
%pidp = getelementptr [16 x i32], ptr @P_pid, i32 0, i32 %h
%pid = load i32, ptr %pidp
%k = call i32 @kill(i32 %pid, i32 9)
br label %out
out:
ret void
}
define void @lu_proc_free(i32 %h) {
entry:
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %out, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%run = icmp eq i32 %st, 1
br i1 %run, label %try, label %drop
try:
%r = call i32 @pr_reap(i32 %h)
%ended = icmp eq i32 %r, 1
br i1 %ended, label %drop, label %orphan
orphan:
store i32 3, ptr %stp
br label %out
drop:
%st2 = load i32, ptr %stp
%gone = icmp eq i32 %st2, 3
br i1 %gone, label %out, label %clear
clear:
store i32 0, ptr %stp
br label %out
out:
ret void
}

View file

@ -0,0 +1,134 @@
# ============================================================================
# process.ludic — the Process.* standard library.
#
# Child processes, started and polled, never waited on. Process.spawn(path, args) starts a
# program directly - no shell, so nothing in an argument is interpreted - with this
# process's environment and working directory, and returns at once with a handle. Each
# frame the game asks Process.poll(h): -1 while the child runs, then its exit code. A child
# killed by a signal (POSIX) answers 128 + the signal, as a shell would report it, and
# Process.kill answers 137 on both platforms. Process.free lets the handle go; it does not
# stop a child that is still running.
#
# This is what a launcher is made of: start the game, hide the launcher's window
# (App.window_hide), notice the game has gone - and whether it crashed - and come back.
#
# The argument list is turned into what each platform wants here, in Ludic: an argv array
# for posix_spawn (process.ll), and one command line quoted by the MSVC rules for
# CreateProcessW (process_win.ll), which starts the child with no console window. Linked
# only into a program that uses Process.*.
#
# Like Http.* and Time.now it is OUT-OF-BAND and must never feed a deterministic
# lockstep/replay simulation.
# ============================================================================
extern function lu_proc_spawn(path: pointer, argv: pointer, cmdline: pointer) -> int = "lu_proc_spawn"
extern function lu_proc_poll(h: int) -> int = "lu_proc_poll"
extern function lu_proc_kill(h: int) -> void = "lu_proc_kill"
extern function lu_proc_free(h: int) -> void = "lu_proc_free"
function proc_cstr_len(p: pointer) -> int {
if p == null { return 0 }
var n = 0
while p[n] != 0 { n += 1 }
return n
}
# does an argument need quoting on a Windows command line?
function proc_needs_quote(a: pointer) -> bool {
if (a == null) or (a[0] == 0) { return true }
var i = 0
while a[i] != 0 {
let c = a[i]
if (c == ' ') or (c == 9) or (c == 10) or (c == 11) or (c == '"') { return true }
i += 1
}
return false
}
# append `a` to `out` at `o`, quoted so CommandLineToArgvW / the MSVC CRT read it back as
# exactly `a`: backslashes are literal unless they run into a quote, where they double
function proc_put_arg(out: pointer, o: int, a: pointer) -> int {
var k = o
if not proc_needs_quote(a) {
var j = 0
while a[j] != 0 { out[k] = a[j]; k += 1; j += 1 }
return k
}
out[k] = '"'; k += 1
var i = 0
var slashes = 0
while (a != null) and (a[i] != 0) {
let c = a[i]
if c == 92 {
slashes += 1
} else if c == '"' {
var s = 0
while s < (slashes * 2) + 1 { out[k] = 92; k += 1; s += 1 }
out[k] = '"'; k += 1
slashes = 0
} else {
var s2 = 0
while s2 < slashes { out[k] = 92; k += 1; s2 += 1 }
out[k] = c; k += 1
slashes = 0
}
i += 1
}
# before the closing quote the backslashes double
var s3 = 0
while s3 < slashes * 2 { out[k] = 92; k += 1; s3 += 1 }
out[k] = '"'; k += 1
return k
}
# The Windows command line for path + args. argv[0] follows its own rule (quotes toggle,
# backslashes are always literal), and a path cannot hold a quote, so it is simply quoted.
function proc_cmdline(path: pointer, args: []pointer) -> pointer {
let n = len(args)
var cap = proc_cstr_len(path) + 4
var i = 0
while i < n { cap += (proc_cstr_len(args[i]) * 2) + 4; i += 1 }
let out = bytes(cap)
var k = 0
out[k] = '"'; k += 1
var j = 0
while path[j] != 0 { out[k] = path[j]; k += 1; j += 1 }
out[k] = '"'; k += 1
i = 0
while i < n {
out[k] = ' '; k += 1
k = proc_put_arg(out, k, args[i])
i += 1
}
out[k] = 0
return out
}
# Start `path` with `args` (argv[0] is `path`), inheriting the environment and the working
# directory. Never blocks. A handle >= 0, or -1 when the program could not be started.
function process_spawn(path: pointer, args: []pointer) -> int {
if (path == null) or (path[0] == 0) { return -1 }
let n = len(args)
var argv: pointers = bytes((n + 2) * 8)
argv[0] = path
var i = 0
while i < n {
var a: pointer = args[i]
if a == null { a = "" }
argv[i + 1] = a
i += 1
}
argv[n + 1] = null
let cmd = proc_cmdline(path, args)
let h = lu_proc_spawn(path, argv, cmd)
free(cmd)
free(argv)
return h
}
# -1 while the child runs, then its exit code (128 + signal for a child a signal ended)
function process_poll(h: int) -> int { return lu_proc_poll(h) }
# end the child now; a later poll answers 137
function process_kill(h: int) -> void { lu_proc_kill(h) }
# let the handle go (a running child keeps running, and is reaped when it ends)
function process_free(h: int) -> void { lu_proc_free(h) }

View file

@ -0,0 +1,196 @@
; ============================================================================
; process_win.ll — child processes behind Process.* (Windows, CreateProcessW).
;
; process.ll's interface. ludicc links it when a Windows program uses Process.*.
;
; lu_proc_spawn(path, argv, cmdline) -> int CreateProcessW(path, cmdline): a handle
; 0..15, or -1. argv is POSIX's and unused;
; cmdline was quoted by process.ludic.
; lu_proc_poll(h) -> int -1 while running, else GetExitCodeProcess
; lu_proc_kill(h) TerminateProcess, exit code 137
; lu_proc_free(h) CloseHandle
;
; The child inherits the environment and the working directory and no handles, and is
; started with CREATE_NO_WINDOW: a GUI parent starting a console program must not flash a
; console window up (a GUI child ignores the flag). Paths and the command line cross in
; UTF-8 and are converted to UTF-16 here.
;
; An exit code is a DWORD. One that reads as -1 (0xFFFFFFFF) is reported as 255, so -1
; always means "still running"; an NTSTATUS crash code such as 0xC0000005 arrives as a
; negative int.
; ============================================================================
declare ptr @malloc(i64)
declare void @free(ptr)
declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
declare i32 @MultiByteToWideChar(i32, i32, ptr, i32, ptr, i32)
declare i32 @CreateProcessW(ptr, ptr, ptr, ptr, i32, i32, ptr, ptr, ptr, ptr)
declare i32 @WaitForSingleObject(ptr, i32)
declare i32 @GetExitCodeProcess(ptr, ptr)
declare i32 @TerminateProcess(ptr, i32)
declare i32 @CloseHandle(ptr)
@P_handle = internal global [16 x ptr] zeroinitializer
@P_state = internal global [16 x i32] zeroinitializer ; 0 free, 1 running, 2 ended
@P_code = internal global [16 x i32] zeroinitializer
define internal i1 @pr_bad(i32 %h) {
entry:
%lo = icmp slt i32 %h, 0
%hi = icmp sgt i32 %h, 15
%bad = or i1 %lo, %hi
ret i1 %bad
}
; a fresh NUL-terminated UTF-16 copy of a UTF-8 string (CP_UTF8 = 65001), or null
define internal ptr @pr_wide(ptr %s) {
entry:
%n = call i32 @MultiByteToWideChar(i32 65001, i32 0, ptr %s, i32 -1, ptr null, i32 0)
%bad = icmp sle i32 %n, 0
br i1 %bad, label %none, label %conv
none:
ret ptr null
conv:
%n64 = sext i32 %n to i64
%bytes = mul i64 %n64, 2
%w = call ptr @malloc(i64 %bytes)
%r = call i32 @MultiByteToWideChar(i32 65001, i32 0, ptr %s, i32 -1, ptr %w, i32 %n)
ret ptr %w
}
define i32 @lu_proc_spawn(ptr %path, ptr %argv, ptr %cmdline) {
entry:
%si = alloca [104 x i8], align 8 ; STARTUPINFOW
%pi = alloca [24 x i8], align 8 ; PROCESS_INFORMATION
br label %find
find:
%i = phi i32 [ 0, %entry ], [ %i1, %next ]
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %i
%st = load i32, ptr %stp
%free = icmp eq i32 %st, 0
br i1 %free, label %conv, label %next
next:
%i1 = add i32 %i, 1
%more = icmp slt i32 %i1, 16
br i1 %more, label %find, label %fail
conv:
%wpath = call ptr @pr_wide(ptr %path)
%wcmd = call ptr @pr_wide(ptr %cmdline)
%np = icmp eq ptr %wpath, null
%nc = icmp eq ptr %wcmd, null
%either = or i1 %np, %nc
br i1 %either, label %cleanup_fail, label %spawn
spawn:
call void @llvm.memset.p0.i64(ptr %si, i8 0, i64 104, i1 false)
store i32 104, ptr %si
call void @llvm.memset.p0.i64(ptr %pi, i8 0, i64 24, i1 false)
; CREATE_NO_WINDOW = 0x08000000; no inherited handles, the parent's environment and directory
%ok = call i32 @CreateProcessW(ptr %wpath, ptr %wcmd, ptr null, ptr null, i32 0, i32 134217728, ptr null, ptr null, ptr %si, ptr %pi)
%failed = icmp eq i32 %ok, 0
br i1 %failed, label %cleanup_fail, label %keep
keep:
call void @free(ptr %wpath)
call void @free(ptr %wcmd)
%hproc = load ptr, ptr %pi
%thp = getelementptr i8, ptr %pi, i64 8
%hthr = load ptr, ptr %thp
%c = call i32 @CloseHandle(ptr %hthr)
%hp = getelementptr [16 x ptr], ptr @P_handle, i32 0, i32 %i
store ptr %hproc, ptr %hp
store i32 1, ptr %stp
%cp = getelementptr [16 x i32], ptr @P_code, i32 0, i32 %i
store i32 -1, ptr %cp
ret i32 %i
cleanup_fail:
%fp = icmp eq ptr %wpath, null
br i1 %fp, label %fc, label %freep
freep:
call void @free(ptr %wpath)
br label %fc
fc:
%fcm = icmp eq ptr %wcmd, null
br i1 %fcm, label %fail, label %freec
freec:
call void @free(ptr %wcmd)
br label %fail
fail:
ret i32 -1
}
define i32 @lu_proc_poll(i32 %h) {
entry:
%code = alloca i32, align 4
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %none, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%cp = getelementptr [16 x i32], ptr @P_code, i32 0, i32 %h
%done = icmp eq i32 %st, 2
br i1 %done, label %cached, label %isrun
isrun:
%run = icmp eq i32 %st, 1
br i1 %run, label %ask, label %none
ask:
%hp = getelementptr [16 x ptr], ptr @P_handle, i32 0, i32 %h
%hproc = load ptr, ptr %hp
%w = call i32 @WaitForSingleObject(ptr %hproc, i32 0)
%ended = icmp eq i32 %w, 0 ; WAIT_OBJECT_0
br i1 %ended, label %getcode, label %running
running:
ret i32 -1
getcode:
store i32 255, ptr %code
%g = call i32 @GetExitCodeProcess(ptr %hproc, ptr %code)
%v = load i32, ptr %code
%neg1 = icmp eq i32 %v, -1
%v2 = select i1 %neg1, i32 255, i32 %v
%gfail = icmp eq i32 %g, 0
%v3 = select i1 %gfail, i32 255, i32 %v2
store i32 %v3, ptr %cp
store i32 2, ptr %stp
ret i32 %v3
cached:
%c = load i32, ptr %cp
ret i32 %c
none:
ret i32 255
}
define void @lu_proc_kill(i32 %h) {
entry:
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %out, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%run = icmp eq i32 %st, 1
br i1 %run, label %go, label %out
go:
%hp = getelementptr [16 x ptr], ptr @P_handle, i32 0, i32 %h
%hproc = load ptr, ptr %hp
%t = call i32 @TerminateProcess(ptr %hproc, i32 137)
br label %out
out:
ret void
}
define void @lu_proc_free(i32 %h) {
entry:
%bad = call i1 @pr_bad(i32 %h)
br i1 %bad, label %out, label %look
look:
%stp = getelementptr [16 x i32], ptr @P_state, i32 0, i32 %h
%st = load i32, ptr %stp
%used = icmp ne i32 %st, 0
br i1 %used, label %close, label %out
close:
%hp = getelementptr [16 x ptr], ptr @P_handle, i32 0, i32 %h
%hproc = load ptr, ptr %hp
%c = call i32 @CloseHandle(ptr %hproc)
store ptr null, ptr %hp
store i32 0, ptr %stp
br label %out
out:
ret void
}

View file

@ -49,6 +49,7 @@ declare ptr @CreateWindowExA(i32, ptr, ptr, i32, i32, i32, i32, i32, ptr, ptr, p
declare i64 @DefWindowProcA(ptr, i32, i64, i64)
declare i32 @ShowWindow(ptr, i32)
declare i32 @SetForegroundWindow(ptr)
declare i32 @BringWindowToTop(ptr)
declare i32 @PeekMessageA(ptr, ptr, i32, i32, i32)
declare i64 @DispatchMessageA(ptr)
declare ptr @GetDC(ptr)
@ -267,6 +268,39 @@ out:
ret void
}
; ---- App.window_hide / App.window_show ------------------------------------------------
; The game's window leaves the screen and comes back with its GL context or Vulkan surface
; untouched: a launcher steps aside while the game it started runs. A hidden window is
; still a window, so the run goes on. Both are no-ops before there is a window.
define void @app_window_hide() {
entry:
%h = load ptr, ptr @W_hwnd
%none = icmp eq ptr %h, null
br i1 %none, label %out, label %go
go:
%r = call i32 @ShowWindow(ptr %h, i32 0) ; SW_HIDE
; keys held as it went never send their key-up here
call void @llvm.memset.p0.i64(ptr @W_held, i8 0, i64 32, i1 false)
store i32 0, ptr @W_mbtn
br label %out
out:
ret void
}
define void @app_window_show() {
entry:
%h = load ptr, ptr @W_hwnd
%none = icmp eq ptr %h, null
br i1 %none, label %out, label %go
go:
%r = call i32 @ShowWindow(ptr %h, i32 5) ; SW_SHOW
%t = call i32 @BringWindowToTop(ptr %h)
%f = call i32 @SetForegroundWindow(ptr %h)
br label %out
out:
ret void
}
; ---- the native window, for a graphics API that makes its own surface ------------------
; A Vulkan swapchain is created on the window itself (VkWin32SurfaceCreateInfoKHR takes the
; module instance and the HWND), not through a context this file attaches. Null before