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:
parent
c1db0d71ed
commit
43d5eb5b88
31 changed files with 89274 additions and 86727 deletions
22
changes/launcher-runtime.md
Normal file
22
changes/launcher-runtime.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**A launcher's runtime** — start a program, step aside while it runs, and download large
|
||||||
|
files with a progress bar.
|
||||||
|
|
||||||
|
- **`Process.*`** — `Process.spawn(path, args)` starts a program directly (no shell) with this
|
||||||
|
process's environment and working directory and returns at once; `Process.poll(h)` is `-1`
|
||||||
|
while it runs, then its exit code (`128 + signal` for a signal death, `137` after
|
||||||
|
`Process.kill`); `Process.free` lets a handle go. `posix_spawn` on macOS
|
||||||
|
(`runtime/native/process.ll`); `CreateProcessW` on Windows (`process_win.ll`), with each
|
||||||
|
argument quoted by the MSVC rules and no console window. Linked only into a program that
|
||||||
|
uses `Process.*`.
|
||||||
|
- **`Http.save_to(h, path)`** — before `Http.send`, streams the response body straight into a
|
||||||
|
file (written at `path` itself, created on send) instead of memory; `Http.text` of such a
|
||||||
|
request is empty and `Http.body_len` is the bytes written. **`Http.received(h)`** and
|
||||||
|
**`Http.expected(h)`** (the `Content-Length`, or `-1`) are live while it is pending. macOS
|
||||||
|
streams through an `NSURLSession` with a delegate built at run time, Windows through the
|
||||||
|
WinHTTP read loop. Freeing a pending request now cancels it and parks its slot until the
|
||||||
|
worker has let go, rather than freeing what the worker is still writing.
|
||||||
|
- **`App.window_hide()` / `App.window_show()`** — take the game's window off the screen and
|
||||||
|
bring it back without closing it or its GL / Vulkan surface; the run goes on while it is
|
||||||
|
hidden. No-ops headless and before there is a window.
|
||||||
|
|
@ -4,4 +4,4 @@ title: App
|
||||||
order: 48
|
order: 48
|
||||||
---
|
---
|
||||||
|
|
||||||
The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before <code>main</code> from the game's asset pack, and <a href="app-splash_hide"><code>App.splash_hide</code></a> takes it down when the game has something to show instead.
|
The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before <code>main</code> from the game's asset pack, and <a href="app-splash_hide"><code>App.splash_hide</code></a> takes it down when the game has something to show instead. <a href="app-window_hide"><code>App.window_hide</code></a> and <a href="app-window_show"><code>App.window_show</code></a> take the game's own window off the screen and bring it back without closing it — a launcher stepping aside while the game it started runs.
|
||||||
|
|
|
||||||
35
docs/language/app/app-window_hide.md
Normal file
35
docs/language/app/app-window_hide.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
id: app-window_hide
|
||||||
|
name: App.window_hide
|
||||||
|
category: app
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: App.window_hide
|
||||||
|
sig: App.window_hide() -> void
|
||||||
|
tip: Take the game's window off the screen.
|
||||||
|
order: 3
|
||||||
|
ns: App
|
||||||
|
member: window_hide
|
||||||
|
---
|
||||||
|
|
||||||
|
Takes the game's window off the screen without closing it: its GL context or Vulkan surface, its size and its contents are kept, and the program goes on running — a hidden window does not end the run. <a href="app-window_show.html"><code>App.window_show</code></a> brings it back. It is how a launcher steps aside while the game it started with <a href="../process/process-spawn.html"><code>Process.spawn</code></a> runs.
|
||||||
|
|
||||||
|
Keep the frame loop going while hidden so the child can be polled, but there is nothing to see: skip the drawing, and throttle the loop yourself — a hidden window is not paced by the display, so an unthrottled loop spins. It does not touch the boot splash (<a href="app-splash_hide.html"><code>App.splash_hide</code></a> does). Safe to call when there is no window yet, and twice. Headless there is no window and the call lowers to nothing.
|
||||||
|
|
||||||
|
```ludic skip
|
||||||
|
program Launcher {
|
||||||
|
var game: int = -1
|
||||||
|
function play() -> void {
|
||||||
|
game = Process.spawn(game_path(), new []string)
|
||||||
|
if game >= 0 { App.window_hide() }
|
||||||
|
}
|
||||||
|
handler Watch phase Update {
|
||||||
|
if game >= 0 and Process.poll(game) != -1 {
|
||||||
|
Process.free(game)
|
||||||
|
game = -1
|
||||||
|
App.window_show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<code>orderOut:</code> on macOS; <code>ShowWindow(SW_HIDE)</code> on Windows, which also forgets any key held as the window went.
|
||||||
26
docs/language/app/app-window_show.md
Normal file
26
docs/language/app/app-window_show.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
id: app-window_show
|
||||||
|
name: App.window_show
|
||||||
|
category: app
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: App.window_show
|
||||||
|
sig: App.window_show() -> void
|
||||||
|
tip: Bring the game's window back.
|
||||||
|
order: 4
|
||||||
|
ns: App
|
||||||
|
member: window_show
|
||||||
|
---
|
||||||
|
|
||||||
|
Puts a window hidden by <a href="app-window_hide.html"><code>App.window_hide</code></a> back on screen, in front and focused, as it was. Safe to call on a window that is already showing, when there is no window, and headless, where it lowers to nothing.
|
||||||
|
|
||||||
|
```ludic skip
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
App.window_hide()
|
||||||
|
wait_for_the_game()
|
||||||
|
App.window_show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS <code>makeKeyAndOrderFront:</code> and the application is activated. On Windows <code>ShowWindow(SW_SHOW)</code> and <code>SetForegroundWindow</code>; Windows may refuse the focus to a program that is not in the foreground, in which case the window comes back and its taskbar button flashes instead.
|
||||||
|
|
@ -4,6 +4,6 @@ title: Http
|
||||||
order: 8
|
order: 8
|
||||||
---
|
---
|
||||||
|
|
||||||
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with <a href="http-get.html"><code>Http.get</code></a> / <a href="http-post.html"><code>Http.post</code></a> (or <a href="http-open.html"><code>Http.open</code></a> to add headers first), then each frame call <a href="http-poll.html"><code>Http.poll</code></a> — it returns <code>-1</code> while pending, <code>0</code> on error, or the status code — so the frame never blocks. Read the reply with <a href="http-status.html"><code>Http.status</code></a> / <a href="http-ok.html"><code>ok</code></a> / <a href="http-text.html"><code>text</code></a> / <a href="http-header.html"><code>header</code></a>, and pair it with <code>Json.*</code> for (de)serialization. TLS is the system's, on by default.
|
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with <a href="http-get.html"><code>Http.get</code></a> / <a href="http-post.html"><code>Http.post</code></a> (or <a href="http-open.html"><code>Http.open</code></a> to add headers first), then each frame call <a href="http-poll.html"><code>Http.poll</code></a> — it returns <code>-1</code> while pending, <code>0</code> on error, or the status code — so the frame never blocks. Read the reply with <a href="http-status.html"><code>Http.status</code></a> / <a href="http-ok.html"><code>ok</code></a> / <a href="http-text.html"><code>text</code></a> / <a href="http-header.html"><code>header</code></a>, and pair it with <code>Json.*</code> for (de)serialization. TLS is the system's, on by default. A large download goes to a file instead of memory with <a href="http-save_to.html"><code>Http.save_to</code></a>, and <a href="http-received.html"><code>Http.received</code></a> / <a href="http-expected.html"><code>expected</code></a> drive a progress bar while it is pending.
|
||||||
|
|
||||||
HTTP depends on the network and the wall clock, so — like <code>Net.*</code> and <code>Time.now</code> — it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation. The transport is macOS-only for now; the raw-response parser (<a href="http-parse.html"><code>Http.parse</code></a>) is pure and portable.
|
HTTP depends on the network and the wall clock, so — like <code>Net.*</code> and <code>Time.now</code> — it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation. The transport is NSURLConnection / NSURLSession on macOS and WinHTTP on Windows; the raw-response parser (<a href="http-parse.html"><code>Http.parse</code></a>) is pure and portable.
|
||||||
|
|
|
||||||
25
docs/language/http/http-expected.md
Normal file
25
docs/language/http/http-expected.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: http-expected
|
||||||
|
name: Http.expected
|
||||||
|
category: http
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Http.expected
|
||||||
|
sig: Http.expected(handle) -> int
|
||||||
|
tip: The body length the server announced, or -1.
|
||||||
|
order: 18
|
||||||
|
ns: Http
|
||||||
|
member: expected
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the length of the response body as the server announced it (<code>Content-Length</code>), or <code>-1</code> while that is not known. For a request given <a href="http-save_to.html"><code>Http.save_to</code></a> it is known as soon as the headers arrive, and stays <code>-1</code> for a reply sent without a length (chunked) — draw an indeterminate bar then. For an ordinary in-memory request it is <code>-1</code> until the reply lands, then the body length.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let h = Http.open("GET", "https://example.com/big.bin")
|
||||||
|
Http.save_to(h, "big.bin.part")
|
||||||
|
Http.send(h)
|
||||||
|
print(Http.expected(h)) # -1: nothing has arrived yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
25
docs/language/http/http-received.md
Normal file
25
docs/language/http/http-received.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: http-received
|
||||||
|
name: Http.received
|
||||||
|
category: http
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Http.received
|
||||||
|
sig: Http.received(handle) -> int
|
||||||
|
tip: Body bytes received so far.
|
||||||
|
order: 17
|
||||||
|
ns: Http
|
||||||
|
member: received
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns how many bytes of the response body have arrived. For a request given <a href="http-save_to.html"><code>Http.save_to</code></a> it is updated live while the request is pending — the bytes already written to the file — which is what a progress bar reads. For an ordinary in-memory request it is <code>0</code> until the reply lands, then the body length. <code>0</code> before <code>Http.send</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
var dl: int = 0
|
||||||
|
handler Bar phase Render {
|
||||||
|
let got = Http.received(handle: dl)
|
||||||
|
let all = Http.expected(handle: dl)
|
||||||
|
if all > 0 { Screen.bar(x: 8, y: 8, width: 200, height: 6, value: got / 1024, max: all / 1024, color: 0x55cc77, back: 0x222222) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
28
docs/language/http/http-save_to.md
Normal file
28
docs/language/http/http-save_to.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: http-save_to
|
||||||
|
name: Http.save_to
|
||||||
|
category: http
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Http.save_to
|
||||||
|
sig: Http.save_to(handle, path) -> void
|
||||||
|
tip: Stream the response body to a file.
|
||||||
|
order: 16
|
||||||
|
ns: Http
|
||||||
|
member: save_to
|
||||||
|
---
|
||||||
|
|
||||||
|
Makes the response body of an <a href="http-open.html"><code>Http.open</code></a>ed request go straight to the file at <code>path</code> as it arrives, instead of into memory — the way to download something large (an update package of hundreds of megabytes) with a progress bar. Call it before <a href="http-send.html"><code>Http.send</code></a>; afterwards it does nothing.
|
||||||
|
|
||||||
|
The file is written directly at <code>path</code>, with no temporary name: <code>Http.send</code> creates it (or truncates an existing one), and a failed or cancelled download leaves whatever had arrived. So download to a name of your own such as <code>"x.nupkg.part"</code> and rename it once <a href="http-poll.html"><code>Http.poll</code></a> reports a good status — a non-2xx reply's body (an error page) is written to the file too. If the file cannot be created the request finishes with status <code>0</code>, as does one whose write fails midway (a full disk).
|
||||||
|
|
||||||
|
For such a request <a href="http-text.html"><code>Http.text</code></a> is empty (<code>null</code>) and <a href="http-body_len.html"><code>Http.body_len</code></a> is the number of bytes written. Follow progress with <a href="http-received.html"><code>Http.received</code></a> and <a href="http-expected.html"><code>Http.expected</code></a>. Freeing the request while it is pending cancels the download. The transfer asks for no compression (<code>Accept-Encoding: identity</code>) unless the request set that header, so the bytes counted are the bytes on disk. Files are limited to 2 GB.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let h = Http.open("GET", "https://example.com/big.bin")
|
||||||
|
Http.save_to(h, "big.bin.part")
|
||||||
|
Http.send(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
9
docs/language/process/_section.md
Normal file
9
docs/language/process/_section.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
id: process
|
||||||
|
title: Process
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
Child processes, started and polled — what a launcher is made of. <a href="process-spawn.html"><code>Process.spawn</code></a> starts a program directly, with no shell, and returns at once; each frame <a href="process-poll.html"><code>Process.poll</code></a> answers <code>-1</code> while the child runs and its exit code once it has ended, so a crash is a non-zero code the game can act on. <a href="process-kill.html"><code>Process.kill</code></a> ends a child and <a href="process-free.html"><code>Process.free</code></a> lets a handle go. Pair it with <a href="../app/app-window_hide.html"><code>App.window_hide</code></a> to step aside while the child runs.
|
||||||
|
|
||||||
|
The child inherits the environment and the working directory. <code>posix_spawn</code> on macOS; <code>CreateProcessW</code> on Windows, with each argument quoted by the MSVC rules and no console window. Like <code>Http.*</code> it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation.
|
||||||
25
docs/language/process/process-free.md
Normal file
25
docs/language/process/process-free.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
id: process-free
|
||||||
|
name: Process.free
|
||||||
|
category: process
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Process.free
|
||||||
|
sig: Process.free(handle) -> void
|
||||||
|
tip: Let a process handle go.
|
||||||
|
order: 3
|
||||||
|
ns: Process
|
||||||
|
member: free
|
||||||
|
---
|
||||||
|
|
||||||
|
Releases the handle so it can be reused. It does <strong>not</strong> stop a child that is still running — use <a href="process-kill.html"><code>Process.kill</code></a> first for that; such a child keeps running and is reaped by the runtime when it ends, so it never lingers as a zombie. After <code>free</code>, polling the old handle answers <code>255</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let h = Process.spawn("/bin/sh", ["-c", "exit 0"])
|
||||||
|
var code = Process.poll(h)
|
||||||
|
while code == -1 { code = Process.poll(h) }
|
||||||
|
Process.free(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/language/process/process-kill.md
Normal file
23
docs/language/process/process-kill.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
id: process-kill
|
||||||
|
name: Process.kill
|
||||||
|
category: process
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Process.kill
|
||||||
|
sig: Process.kill(handle) -> void
|
||||||
|
tip: End a running child.
|
||||||
|
order: 2
|
||||||
|
ns: Process
|
||||||
|
member: kill
|
||||||
|
---
|
||||||
|
|
||||||
|
Ends the child at once — <code>SIGKILL</code> on macOS, <code>TerminateProcess</code> on Windows — with no chance for it to save anything. A later <a href="process-poll.html"><code>Process.poll</code></a> answers <code>137</code> once it has gone. Does nothing for a child that has already ended, or a handle that is not one.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let h = Process.spawn("/bin/sleep", ["30"])
|
||||||
|
Process.kill(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
32
docs/language/process/process-poll.md
Normal file
32
docs/language/process/process-poll.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
id: process-poll
|
||||||
|
name: Process.poll
|
||||||
|
category: process
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Process.poll
|
||||||
|
sig: Process.poll(handle) -> int
|
||||||
|
tip: -1 while running, else the exit code.
|
||||||
|
order: 1
|
||||||
|
ns: Process
|
||||||
|
member: poll
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns <code>-1</code> while the child is still running and its exit code once it has ended; it never waits. The code is kept, so asking again gives the same answer until <a href="process-free.html"><code>Process.free</code></a>.
|
||||||
|
|
||||||
|
A child ended by a signal (macOS) answers <code>128</code> plus the signal number, as a shell reports it — <code>137</code> after <a href="process-kill.html"><code>Process.kill</code></a>, on both platforms. On Windows the code is the child's DWORD exit code as an <code>int</code>, so a crash such as <code>0xC0000005</code> arrives as a negative number (test for <code>!= 0</code>, not <code>> 0</code>); a code of <code>0xFFFFFFFF</code> is reported as <code>255</code> so that <code>-1</code> always means "still running". A handle that is not one answers <code>255</code>.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
var game: int = -1
|
||||||
|
handler Watch phase Update {
|
||||||
|
if game >= 0 {
|
||||||
|
let code = Process.poll(handle: game)
|
||||||
|
if code >= 0 or code < -1 {
|
||||||
|
if code != 0 { print("the game crashed") }
|
||||||
|
Process.free(handle: game)
|
||||||
|
game = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/process/process-spawn.md
Normal file
27
docs/language/process/process-spawn.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: process-spawn
|
||||||
|
name: Process.spawn
|
||||||
|
category: process
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Process.spawn
|
||||||
|
sig: Process.spawn(path, args) -> int
|
||||||
|
tip: Start a program; never blocks.
|
||||||
|
order: 0
|
||||||
|
ns: Process
|
||||||
|
member: spawn
|
||||||
|
---
|
||||||
|
|
||||||
|
Starts the program at <code>path</code> with the arguments in <code>args</code> (a <code>[]string</code>; the child's <code>argv[0]</code> is <code>path</code>) and returns a handle <code>>= 0</code> straight away, or <code>-1</code> when the program could not be started — a missing file, a file that is not executable, or all 16 handles in use.
|
||||||
|
|
||||||
|
No shell is involved, so every argument reaches the child exactly as written: spaces, quotes and <code>$</code> are not interpreted. The child inherits this process's environment and working directory. On Windows the arguments are joined into one command line quoted so the child's C runtime splits it back into the same list, the path and arguments cross as UTF-8 and are converted to UTF-16, and the child is started without a console window (a GUI program is unaffected; a console program runs hidden). Use an absolute path: there is no <code>PATH</code> search.
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
entry {
|
||||||
|
let h = Process.spawn("/bin/sh", ["-c", "exit 3"])
|
||||||
|
print(h >= 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See <a href="process-poll.html"><code>Process.poll</code></a> for the exit code.
|
||||||
33
examples/library/http_save.ludic
Normal file
33
examples/library/http_save.ludic
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# http_save.ludic — Http.save_to / received / expected: a download streamed to a file.
|
||||||
|
#
|
||||||
|
# let h = Http.open("GET", "https://example.com/big.nupkg")
|
||||||
|
# Http.save_to(h, "big.nupkg.part") # before send: the body goes to the file
|
||||||
|
# Http.send(h)
|
||||||
|
# handler Update {
|
||||||
|
# if Http.poll(h) < 0 { # pending: a progress bar
|
||||||
|
# draw_bar(Http.received(h), Http.expected(h)) # expected is -1 until known
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Run as: http_save <source file> <destination file>. The source is read through a
|
||||||
|
# file:// URL (macOS reads those with the same streaming transport), so the check needs
|
||||||
|
# no network: it copies the file and prints the status, the byte counts and whether the
|
||||||
|
# body stayed out of memory. `cmp` the two files afterwards.
|
||||||
|
program HttpSave {
|
||||||
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||||
|
|
||||||
|
entry {
|
||||||
|
let src = Os.arg(1)
|
||||||
|
let dst = Os.arg(2)
|
||||||
|
let h = Http.open("GET", `file://{src}`)
|
||||||
|
Http.save_to(h, dst)
|
||||||
|
let before = Http.received(h)
|
||||||
|
let unknown = Http.expected(h)
|
||||||
|
Http.send(h)
|
||||||
|
var st = Http.poll(h)
|
||||||
|
while st < 0 { st = Http.poll(h) }
|
||||||
|
let n = Http.received(h)
|
||||||
|
print(`{before} {unknown} {st} {bi(n > 0)} {bi(Http.expected(h) == n)} {bi(Http.body_len(h) == n)} {bi(Http.text(h) == null)}`)
|
||||||
|
Http.free(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
examples/library/process.ludic
Normal file
42
examples/library/process.ludic
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# process.ludic — Process.* : start a child, poll it without waiting, read its exit code.
|
||||||
|
#
|
||||||
|
# let h = Process.spawn("/path/to/game", ["--profile", "Maroon Lake"])
|
||||||
|
# handler Update { # each frame, never blocking
|
||||||
|
# let code = Process.poll(h) # -1 while it runs, then the exit code
|
||||||
|
# if code >= 0 { Process.free(h) }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# No shell is involved: every argument arrives in the child exactly as given. This run
|
||||||
|
# uses /bin/sh as the child (so it is a POSIX check) and prints:
|
||||||
|
# 3 1 7 137 -1 255
|
||||||
|
program ProcessDemo {
|
||||||
|
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||||
|
|
||||||
|
entry {
|
||||||
|
# a child that takes a second and exits 3: poll answers -1 many times, then 3
|
||||||
|
let a = Process.spawn("/bin/sh", ["-c", "sleep 1; exit 3"])
|
||||||
|
var polls = 0
|
||||||
|
var code = Process.poll(a)
|
||||||
|
while code < 0 { polls += 1; code = Process.poll(a) }
|
||||||
|
Process.free(a)
|
||||||
|
|
||||||
|
# arguments are passed through untouched - spaces, quotes and all
|
||||||
|
let b = Process.spawn("/bin/sh", ["-c", "test \"$1\" = 'a b\"c' && exit 7; exit 1", "sh", "a b\"c"])
|
||||||
|
var cb = Process.poll(b)
|
||||||
|
while cb < 0 { cb = Process.poll(b) }
|
||||||
|
Process.free(b)
|
||||||
|
|
||||||
|
# a long child, killed: 128 + SIGKILL
|
||||||
|
let c = Process.spawn("/bin/sleep", ["30"])
|
||||||
|
let running = Process.poll(c)
|
||||||
|
Process.kill(c)
|
||||||
|
var cc = Process.poll(c)
|
||||||
|
while cc < 0 { cc = Process.poll(c) }
|
||||||
|
Process.free(c)
|
||||||
|
|
||||||
|
# a program that is not there, and a handle that is not one
|
||||||
|
let d = Process.spawn("/definitely/not/here", new []string)
|
||||||
|
print(`{code} {bi(polls > 100)} {cb} {cc} {d} {Process.poll(99)}`)
|
||||||
|
if running != -1 { print("sleep was not running") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -147,6 +147,7 @@ declare i32 @CGWarpMouseCursorPosition(%NSPoint)
|
||||||
@W_scale = internal global i32 3
|
@W_scale = internal global i32 3
|
||||||
@W_key = internal global i32 0
|
@W_key = internal global i32 0
|
||||||
@W_running = internal global i32 1
|
@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,
|
; #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).
|
; and the mouse state (position, button mask, per-frame wheel delta).
|
||||||
@W_held = internal global [8 x i32] zeroinitializer
|
@W_held = internal global [8 x i32] zeroinitializer
|
||||||
|
|
@ -565,6 +566,10 @@ finish:
|
||||||
%hw = icmp ne ptr %win, null
|
%hw = icmp ne ptr %win, null
|
||||||
br i1 %hw, label %checkvis, label %done
|
br i1 %hw, label %checkvis, label %done
|
||||||
checkvis:
|
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)
|
%sel_vis = call ptr @sel_registerName(ptr @.s_visib)
|
||||||
%vis = call i8 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_vis)
|
%vis = call i8 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_vis)
|
||||||
%gone = icmp eq i8 %vis, 0
|
%gone = icmp eq i8 %vis, 0
|
||||||
|
|
@ -1695,6 +1700,47 @@ out:
|
||||||
ret void
|
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 ------------------
|
; ---- 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
|
; 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.
|
; off it); there is no module instance to hand over, so that one is null.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,15 @@
|
||||||
; http_done(slot) -> bool http_status(slot) -> int
|
; http_done(slot) -> bool http_status(slot) -> int
|
||||||
; http_body(slot) -> ptr http_blen(slot) -> int
|
; http_body(slot) -> ptr http_blen(slot) -> int
|
||||||
; http_header(slot, name) -> cstr http_free(slot)
|
; 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)
|
declare ptr @objc_getClass(ptr)
|
||||||
|
|
@ -31,6 +40,15 @@ declare void @free(ptr)
|
||||||
declare ptr @memcpy(ptr, ptr, i64)
|
declare ptr @memcpy(ptr, ptr, i64)
|
||||||
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
|
declare i32 @pthread_create(ptr, ptr, ptr, ptr)
|
||||||
declare i32 @pthread_detach(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_nsurl = private unnamed_addr constant [6 x i8] c"NSURL\00"
|
||||||
@.h_nsstr = private unnamed_addr constant [9 x i8] c"NSString\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_rel = private unnamed_addr constant [8 x i8] c"release\00"
|
||||||
@.h_empty = private unnamed_addr constant [1 x i8] c"\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
|
; HTTP_SLOTS = 8
|
||||||
@H_req = internal global [8 x ptr] zeroinitializer
|
@H_req = internal global [8 x ptr] zeroinitializer
|
||||||
@H_resp = 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_blen = internal global [8 x i32] zeroinitializer
|
||||||
@H_status = internal global [8 x i32] zeroinitializer
|
@H_status = internal global [8 x i32] zeroinitializer
|
||||||
@H_done = 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 ---------------------------------------------------
|
; --- request construction ---------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -218,6 +265,14 @@ entry:
|
||||||
store ptr null, ptr %respp
|
store ptr null, ptr %respp
|
||||||
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
||||||
store ptr %req, ptr %reqp
|
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
|
%tid = alloca i64
|
||||||
%arg = inttoptr i32 %slot to ptr
|
%arg = inttoptr i32 %slot to ptr
|
||||||
%rc = call i32 @pthread_create(ptr %tid, ptr null, ptr @hs_worker, ptr %arg)
|
%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
|
%nores = icmp eq ptr %resp, null
|
||||||
br i1 %nores, label %empty, label %ask
|
br i1 %nores, label %empty, label %ask
|
||||||
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)
|
%strcls = call ptr @objc_getClass(ptr @.h_nsstr)
|
||||||
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
|
%sel_u = call ptr @sel_registerName(ptr @.h_utf8)
|
||||||
%ns = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %strcls, ptr %sel_u, ptr %name)
|
%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)
|
%val = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %resp, ptr %sel_vh, ptr %ns)
|
||||||
%noval = icmp eq ptr %val, null
|
%noval = icmp eq ptr %val, null
|
||||||
br i1 %noval, label %empty, label %tocstr
|
br i1 %noval, label %empty, label %tocstr
|
||||||
|
|
@ -302,5 +363,346 @@ rr:
|
||||||
store ptr null, ptr %rp
|
store ptr null, ptr %rp
|
||||||
br label %out
|
br label %out
|
||||||
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
|
ret void
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@
|
||||||
# any target and is what the test suite exercises without touching the network.
|
# 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
|
const HTTP_SLOTS: int = 8
|
||||||
|
|
||||||
var h_ready: bool = false
|
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_req: pointers = null # the pending NSURLRequest (native, before send)
|
||||||
var h_body: pointers = null # body bytes (NUL-terminated); owned by the slot
|
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_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 {
|
function http_init() -> void {
|
||||||
if h_ready { return }
|
if h_ready { return }
|
||||||
|
|
@ -42,11 +51,13 @@ function http_init() -> void {
|
||||||
h_req = bytes(HTTP_SLOTS * 8)
|
h_req = bytes(HTTP_SLOTS * 8)
|
||||||
h_body = bytes(HTTP_SLOTS * 8)
|
h_body = bytes(HTTP_SLOTS * 8)
|
||||||
h_hdr = bytes(HTTP_SLOTS * 8)
|
h_hdr = bytes(HTTP_SLOTS * 8)
|
||||||
|
h_save = words(HTTP_SLOTS)
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < HTTP_SLOTS {
|
while i < HTTP_SLOTS {
|
||||||
h_used[i] = 0; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
|
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_status[i] = 0; h_blen[i] = 0
|
||||||
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
||||||
|
h_save[i] = 0
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
h_ready = true
|
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_used[i] = 1; h_native[i] = 0; h_sent[i] = 0; h_res[i] = 0
|
||||||
h_status[i] = 0; h_blen[i] = 0
|
h_status[i] = 0; h_blen[i] = 0
|
||||||
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
h_req[i] = null; h_body[i] = null; h_hdr[i] = null
|
||||||
|
h_save[i] = 0
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
|
@ -70,7 +82,22 @@ function http_slot_alloc() -> int {
|
||||||
function http_valid(h: int) -> bool {
|
function http_valid(h: int) -> bool {
|
||||||
http_init()
|
http_init()
|
||||||
if (h < 1) or (h > HTTP_SLOTS) { return false }
|
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 -----------------------------------
|
# ---- length of a NUL-terminated C string -----------------------------------
|
||||||
|
|
@ -84,6 +111,7 @@ function http_cstr_len(p: pointer) -> int {
|
||||||
# ---- request construction (native) -----------------------------------------
|
# ---- request construction (native) -----------------------------------------
|
||||||
# Open a request without sending it, so headers / a body can be added first.
|
# Open a request without sending it, so headers / a body can be added first.
|
||||||
function http_open(method: pointer, url: pointer) -> int {
|
function http_open(method: pointer, url: pointer) -> int {
|
||||||
|
http_reap()
|
||||||
let req = hs_req_new(method, url)
|
let req = hs_req_new(method, url)
|
||||||
if req == null { return 0 }
|
if req == null { return 0 }
|
||||||
let s = http_slot_alloc()
|
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))
|
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.
|
# Dispatch the request onto the background worker.
|
||||||
function http_send_req(h: int) -> void {
|
function http_send_req(h: int) -> void {
|
||||||
if not http_valid(h) { return }
|
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 {
|
function http_close(h: int) -> void {
|
||||||
if not http_valid(h) { return }
|
if not http_valid(h) { return }
|
||||||
let s = h - 1
|
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 {
|
else {
|
||||||
if h_body[s] != null { free(h_body[s]) }
|
if h_body[s] != null { free(h_body[s]) }
|
||||||
if h_hdr[s] != null { free(h_hdr[s]) }
|
if h_hdr[s] != null { free(h_hdr[s]) }
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@
|
||||||
; hs_body(slot) -> ptr hs_blen(slot) -> int
|
; hs_body(slot) -> ptr hs_blen(slot) -> int
|
||||||
; hs_header(slot, name) -> cstr hs_free(slot)
|
; 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.
|
; 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,
|
; A request record: method 0, url 8, header lines 16 ("Name: value\r\n"...), body 24,
|
||||||
; body length 32.
|
; body length 32.
|
||||||
|
|
@ -44,6 +49,8 @@ declare i32 @WinHttpQueryHeaders(ptr, i32, ptr, ptr, ptr, ptr)
|
||||||
declare i32 @WinHttpQueryDataAvailable(ptr, ptr)
|
declare i32 @WinHttpQueryDataAvailable(ptr, ptr)
|
||||||
declare i32 @WinHttpReadData(ptr, ptr, i32, ptr)
|
declare i32 @WinHttpReadData(ptr, ptr, i32, ptr)
|
||||||
declare i32 @WinHttpCloseHandle(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_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"
|
@.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_status = internal global [8 x i32] zeroinitializer
|
||||||
@H_done = internal global [8 x i32] zeroinitializer
|
@H_done = internal global [8 x i32] zeroinitializer
|
||||||
@H_hdr = internal global [8 x ptr] 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 -----------------------------------------------------------------
|
; ---- strings -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -220,6 +233,23 @@ entry:
|
||||||
%murl = load ptr, ptr %murlp
|
%murl = load ptr, ptr %murlp
|
||||||
%wurl = call ptr @h_wide(ptr %murl)
|
%wurl = call ptr @h_wide(ptr %murl)
|
||||||
store ptr %wurl, ptr %wurlp
|
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
|
; 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)
|
%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
|
%sessp = getelementptr [8 x ptr], ptr @H_sess, i32 0, i32 %slot
|
||||||
|
|
@ -307,6 +337,71 @@ status:
|
||||||
%st = load i32, ptr %num
|
%st = load i32, ptr %num
|
||||||
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
|
%stp = getelementptr [8 x i32], ptr @H_status, i32 0, i32 %slot
|
||||||
store i32 %st, ptr %stp
|
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)
|
%b0 = call ptr @malloc(i64 4097)
|
||||||
store ptr %b0, ptr %bufp
|
store ptr %b0, ptr %bufp
|
||||||
store i64 0, ptr %lenp
|
store i64 0, ptr %lenp
|
||||||
|
|
@ -369,6 +464,17 @@ done:
|
||||||
call void @h_free_if(ptr %f3)
|
call void @h_free_if(ptr %f3)
|
||||||
%f4 = load ptr, ptr %whdrp
|
%f4 = load ptr, ptr %whdrp
|
||||||
call void @h_free_if(ptr %f4)
|
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
|
%donep = getelementptr [8 x i32], ptr @H_done, i32 0, i32 %slot
|
||||||
store atomic i32 1, ptr %donep release, align 4
|
store atomic i32 1, ptr %donep release, align 4
|
||||||
ret i32 0
|
ret i32 0
|
||||||
|
|
@ -387,6 +493,12 @@ entry:
|
||||||
store i32 0, ptr %blenp
|
store i32 0, ptr %blenp
|
||||||
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
%reqp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
||||||
store ptr %req, ptr %reqp
|
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
|
%nil = icmp eq ptr %req, null
|
||||||
br i1 %nil, label %nothing, label %start
|
br i1 %nil, label %nothing, label %start
|
||||||
nothing:
|
nothing:
|
||||||
|
|
@ -510,6 +622,10 @@ entry:
|
||||||
%ss = load ptr, ptr %ssp
|
%ss = load ptr, ptr %ssp
|
||||||
call void @h_close_if(ptr %ss)
|
call void @h_close_if(ptr %ss)
|
||||||
store ptr null, ptr %ssp
|
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
|
%recp = getelementptr [8 x ptr], ptr @H_req, i32 0, i32 %slot
|
||||||
%rec = load ptr, ptr %recp
|
%rec = load ptr, ptr %recp
|
||||||
%norec = icmp eq ptr %rec, null
|
%norec = icmp eq ptr %rec, null
|
||||||
|
|
@ -532,3 +648,38 @@ freerec:
|
||||||
out:
|
out:
|
||||||
ret void
|
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
187
runtime/native/process.ll
Normal 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
|
||||||
|
}
|
||||||
134
runtime/native/process.ludic
Normal file
134
runtime/native/process.ludic
Normal 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) }
|
||||||
196
runtime/native/process_win.ll
Normal file
196
runtime/native/process_win.ll
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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 i64 @DefWindowProcA(ptr, i32, i64, i64)
|
||||||
declare i32 @ShowWindow(ptr, i32)
|
declare i32 @ShowWindow(ptr, i32)
|
||||||
declare i32 @SetForegroundWindow(ptr)
|
declare i32 @SetForegroundWindow(ptr)
|
||||||
|
declare i32 @BringWindowToTop(ptr)
|
||||||
declare i32 @PeekMessageA(ptr, ptr, i32, i32, i32)
|
declare i32 @PeekMessageA(ptr, ptr, i32, i32, i32)
|
||||||
declare i64 @DispatchMessageA(ptr)
|
declare i64 @DispatchMessageA(ptr)
|
||||||
declare ptr @GetDC(ptr)
|
declare ptr @GetDC(ptr)
|
||||||
|
|
@ -267,6 +268,39 @@ out:
|
||||||
ret void
|
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 ------------------
|
; ---- the native window, for a graphics API that makes its own surface ------------------
|
||||||
; A Vulkan swapchain is created on the window itself (VkWin32SurfaceCreateInfoKHR takes the
|
; 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
|
; module instance and the HWND), not through a context this file attaches. Null before
|
||||||
|
|
|
||||||
|
|
@ -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`) }
|
if g_windowed { emit(` call void @lp_app_icon(ptr {ip})\n`) }
|
||||||
return val("0", "void")
|
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}`)
|
perr(`unknown builtin App.{meth}`)
|
||||||
}
|
}
|
||||||
if (ns == "Pool") {
|
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 == "header") { bare = "http_header_of"; push(labels, "handle"); push(labels, "name") }
|
||||||
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
|
if (meth == "free") { bare = "http_close"; push(labels, "handle") }
|
||||||
if (meth == "parse") { bare = "http_parse"; push(labels, "bytes"); push(labels, "len") }
|
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).
|
# 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.
|
# 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") { bare = "udp_ip"; push(labels, "text") }
|
||||||
if (meth == "ip_text") { bare = "udp_ip_text"; push(labels, "ip") }
|
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.
|
# Phase 3: the bare reflection / networking / process builtins, namespaced.
|
||||||
# Each is a pure alias — the callee is rewritten to the bare name below.
|
# Each is a pure alias — the callee is rewritten to the bare name below.
|
||||||
if (ns == "World") {
|
if (ns == "World") {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ function emit_pak_prelude() -> void {
|
||||||
emith("declare void @splash_show(ptr, i32, i32)\n")
|
emith("declare void @splash_show(ptr, i32, i32)\n")
|
||||||
emith("declare void @splash_hide()\n")
|
emith("declare void @splash_hide()\n")
|
||||||
emith("declare void @app_set_icon(ptr, i32)\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()
|
emit_pak_mount()
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,8 @@ function p_postfix() -> Node {
|
||||||
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
|
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).
|
# 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 }
|
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).
|
# 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 }
|
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)
|
# 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_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_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_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_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
|
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
|
# 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")
|
do_import("runtime/native/udp.ludic")
|
||||||
cur_dir = saved
|
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
|
# 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
|
# 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.
|
# is_windowed()-guarded, so a headless build renders into an offscreen context.
|
||||||
|
|
|
||||||
87167
selfhost/ludicc.seed.ll
87167
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -279,6 +279,12 @@ entry {
|
||||||
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/udp_win.ll")} -lws2_32` }
|
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")}` }
|
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_uses_http {
|
||||||
if g_target_win {
|
if g_target_win {
|
||||||
let httpw = join_path(home, "runtime/native/http_win.ll")
|
let httpw = join_path(home, "runtime/native/http_win.ll")
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
||||||
|
|
||||||
if mode == 2 {
|
if mode == 2 {
|
||||||
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
|
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
|
||||||
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||||
if not save { shell(`rm -f {ll}`) }
|
if not save { shell(`rm -f {ll}`) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
|
||||||
# canonical `ludicc -o` path links it only when Audio.* is used.
|
# canonical `ludicc -o` path links it only when Audio.* is used.
|
||||||
let cocoa = `{home}runtime/native/cocoa.ll`
|
let cocoa = `{home}runtime/native/cocoa.ll`
|
||||||
let audio = `{home}runtime/native/audio.ll`
|
let audio = `{home}runtime/native/audio.ll`
|
||||||
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
|
||||||
if not save { shell(`rm -f {ll}`) }
|
if not save { shell(`rm -f {ll}`) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -98,6 +98,13 @@ function udp_link_flags(ll: pointer) -> pointer {
|
||||||
return ` {ludic_home()}runtime/native/udp.ll`
|
return ` {ludic_home()}runtime/native/udp.ll`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A program that uses Process.* calls the lu_proc_* layer; link process.ll only then, as
|
||||||
|
# `ludicc -o` does (Windows links through ludicc, with process_win.ll).
|
||||||
|
function process_link_flags(ll: pointer) -> pointer {
|
||||||
|
if not shq(`grep -q "call .*@lu_proc_" {ll}`) { return "" }
|
||||||
|
return ` {ludic_home()}runtime/native/process.ll`
|
||||||
|
}
|
||||||
|
|
||||||
# A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link
|
# A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link
|
||||||
# threads.ll only then, as `ludicc -o` does.
|
# threads.ll only then, as `ludicc -o` does.
|
||||||
function threads_link_flags(ll: pointer) -> pointer {
|
function threads_link_flags(ll: pointer) -> pointer {
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ function pack_probe_src() -> pointer {
|
||||||
# unreadable image is a no-op rather than a crash or a cleared icon, and that a headless
|
# unreadable image is a no-op rather than a crash or a cleared icon, and that a headless
|
||||||
# build (which has no AppKit linked at all) compiles it away to nothing.
|
# build (which has no AppKit linked at all) compiles it away to nothing.
|
||||||
function app_icon_case() -> void {
|
function app_icon_case() -> void {
|
||||||
let lbl = "App.set_icon -> an icon for a binary with no bundle, and nothing headless"
|
let lbl = "App.set_icon / window_hide / window_show -> windowed no-ops without a window, nothing headless"
|
||||||
if not is_darwin() { skip(lbl); return }
|
if not is_darwin() { skip(lbl); return }
|
||||||
let work = `{tmp_dir()}/appicon`
|
let work = `{tmp_dir()}/appicon`
|
||||||
let root = capture_line("pwd")
|
let root = capture_line("pwd")
|
||||||
|
|
@ -174,6 +174,8 @@ function app_icon_case() -> void {
|
||||||
src = src + " entry {" + nl()
|
src = src + " entry {" + nl()
|
||||||
src = src + " App.set_icon(\"icon.png\")" + nl()
|
src = src + " App.set_icon(\"icon.png\")" + nl()
|
||||||
src = src + " App.set_icon(\"nope.png\")" + nl() # missing: must not crash
|
src = src + " App.set_icon(\"nope.png\")" + nl() # missing: must not crash
|
||||||
|
src = src + " App.window_hide()" + nl() # no window yet: both are no-ops
|
||||||
|
src = src + " App.window_show()" + nl()
|
||||||
src = src + " print(\"ok\")" + nl()
|
src = src + " print(\"ok\")" + nl()
|
||||||
src = src + " }" + nl()
|
src = src + " }" + nl()
|
||||||
src = src + "}" + nl()
|
src = src + "}" + nl()
|
||||||
|
|
@ -730,6 +732,16 @@ function cmd_dev_test() -> int {
|
||||||
if (got2 == "200 1 5 hello 42 text/plain 404 0") { ok("ludic build links http.ll for a program that uses Http.*") }
|
if (got2 == "200 1 5 hello 42 text/plain 404 0") { ok("ludic build links http.ll for a program that uses Http.*") }
|
||||||
else { bad2("http.ludic via ludic build", `got [{got2}]`) }
|
else { bad2("http.ludic via ludic build", `got [{got2}]`) }
|
||||||
} else { bad2("ludic build of an Http.* program", capture_line(`tail -1 {tmp_dir()}/http_lb.out`)) }
|
} else { bad2("ludic build of an Http.* program", capture_line(`tail -1 {tmp_dir()}/http_lb.out`)) }
|
||||||
|
# Http.save_to: a body streamed to a file, counted as it goes - through a file:// URL,
|
||||||
|
# so no network is touched; the copy has to be byte-identical
|
||||||
|
if shq(`LUDIC_HOME=. bin/ludicc examples/library/http_save.ludic -o {tmp_dir()}/http_save > {tmp_dir()}/http_save.out 2>&1`) {
|
||||||
|
let root = capture_line("pwd")
|
||||||
|
shell(`rm -f {tmp_dir()}/http_save.copy`)
|
||||||
|
let got3 = capture_line(`{tmp_dir()}/http_save {root}/selfhost/ludicc.seed.ll {tmp_dir()}/http_save.copy < /dev/null`)
|
||||||
|
let same = shq(`cmp -s {root}/selfhost/ludicc.seed.ll {tmp_dir()}/http_save.copy`)
|
||||||
|
if (got3 == "0 -1 200 1 1 1 1") and same { ok("http_save.ludic (Http.save_to/received/expected - a streamed download, byte for byte)") }
|
||||||
|
else { bad2("http_save.ludic", `got [{got3}] identical {same}`) }
|
||||||
|
} else { bad2("http_save.ludic build", capture_line(`tail -1 {tmp_dir()}/http_save.out`)) }
|
||||||
}
|
}
|
||||||
# Udp.*: two sockets on the loopback, a datagram each way, through ludicc and ludic build
|
# Udp.*: two sockets on the loopback, a datagram each way, through ludicc and ludic build
|
||||||
if is_darwin() {
|
if is_darwin() {
|
||||||
|
|
@ -745,6 +757,21 @@ function cmd_dev_test() -> int {
|
||||||
else { bad2("udp.ludic via ludic build", `got [{got2}]`) }
|
else { bad2("udp.ludic via ludic build", `got [{got2}]`) }
|
||||||
} else { bad2("ludic build of a Udp.* program", capture_line(`tail -1 {tmp_dir()}/udp_lb.out`)) }
|
} else { bad2("ludic build of a Udp.* program", capture_line(`tail -1 {tmp_dir()}/udp_lb.out`)) }
|
||||||
}
|
}
|
||||||
|
# Process.*: a child polled to its exit code without blocking, argv passed untouched, a
|
||||||
|
# kill, a missing program - through ludicc and ludic build (the children are /bin/sh)
|
||||||
|
if is_darwin() {
|
||||||
|
let proc_want = "3 1 7 137 -1 255"
|
||||||
|
if shq(`LUDIC_HOME=. bin/ludicc examples/library/process.ludic -o {tmp_dir()}/proc_demo > {tmp_dir()}/proc.out 2>&1`) {
|
||||||
|
let got = capture_line(`{tmp_dir()}/proc_demo < /dev/null`)
|
||||||
|
if (got == proc_want) { ok("process.ludic (Process.spawn/poll/kill/free - child processes, polled)") }
|
||||||
|
else { bad2("process.ludic", `got [{got}]`) }
|
||||||
|
} else { bad2("process.ludic build", capture_line(`tail -1 {tmp_dir()}/proc.out`)) }
|
||||||
|
if shq(`LUDIC_HOME=. bin/ludic build examples/library/process.ludic --headless -o {tmp_dir()}/proc_lb > {tmp_dir()}/proc_lb.out 2>&1`) {
|
||||||
|
let got2 = capture_line(`{tmp_dir()}/proc_lb < /dev/null`)
|
||||||
|
if (got2 == proc_want) { ok("ludic build links process.ll for a program that uses Process.*") }
|
||||||
|
else { bad2("process.ludic via ludic build", `got [{got2}]`) }
|
||||||
|
} else { bad2("ludic build of a Process.* program", capture_line(`tail -1 {tmp_dir()}/proc_lb.out`)) }
|
||||||
|
}
|
||||||
|
|
||||||
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
feat_case("events/recurse", "", "16", "recurse.ludic (EV6: re-entrant emit is depth-bounded, no runaway cycle)")
|
||||||
net_case("events/scoped", "2")
|
net_case("events/scoped", "2")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue