The browser is a third target, and emcc is its driver
flan build --target=web produces a page, its JS and a .wasm. The two wasm
targets share the word and almost nothing else, so is_wasi and is_web are
separate predicates and is_wasm is their union — the union is exactly the
facts about the machine, 32-bit pointers and no dlopen, which is what the
refusals are about.
Everything the wasi target has to find by hand is what emcc already is: no
sysroot, no builtins archive, no shadow resource directory, and no
__main_argc_argv shim, because emscripten's start code calls main under that
name. target_flags for web is empty and the only thing checked is that emcc
exists. The one fact this rests on is that emcc takes a .ll on its command
line, so Emit's output needs no change.
The main loop is -sASYNCIFY rather than emscripten_set_main_loop, which
BUILT.md predicted. The prediction had the browser right and the cost wrong:
set_main_loop wants the loop body as a callback, so every example that writes
(until (rl/window-should-close?) ...) would be split by hand into an init and
a tick and would stop being the native program. raylib's web platform is built
for asyncify instead — WindowShouldClose on PLATFORM_WEB is an
emscripten_sleep(16) that returns false — so the loop yields at a call it
already makes and no example changed a character. Asyncify goes on every web
link, because whether a program blocks is not a question Build can answer and
a per-program flag set is a per-program cache key.
A link line may now be addressed to one target — @native, @wasi, @web — and
${NAME} expands from the environment. The selection is here and not in Load,
which reads the file, because Load resolves imports before a target is chosen.
The object cache now keys on whichever compiler the target uses, so an emcc
object and a clang one of the same source cannot collide. The refusals name
the target that was asked for; --sanitize on web says the weaker truth, that
emscripten ships an ASan and nothing here has ever run it.
This commit is contained in:
parent
3afce2aeac
commit
c2dc4d4244
11
bin/main.ml
11
bin/main.ml
@ -81,7 +81,8 @@ let sanitize_flag = "--sanitize"
|
|||||||
|
|
||||||
let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag ]
|
let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag ]
|
||||||
|
|
||||||
(* [--target=wasm32-wasi], the one cross target. Unlike the flags above it
|
(* [--target=wasm32-wasi] and [--target=web], the two cross targets. Unlike
|
||||||
|
the flags above, a target
|
||||||
carries a value, so it is matched by prefix and stripped from the residual
|
carries a value, so it is matched by prefix and stripped from the residual
|
||||||
arguments by the same test — otherwise [-o out --target=X] falls into the
|
arguments by the same test — otherwise [-o out --target=X] falls into the
|
||||||
usage error. *)
|
usage error. *)
|
||||||
@ -187,13 +188,17 @@ let () =
|
|||||||
let base = Filename.remove_extension (Filename.basename path) in
|
let base = Filename.remove_extension (Filename.basename path) in
|
||||||
(* A wasm module is not an executable and must not be named like one:
|
(* A wasm module is not an executable and must not be named like one:
|
||||||
the extension is what tells a runtime, and a reader, what it is. *)
|
the extension is what tells a runtime, and a reader, what it is. *)
|
||||||
|
(* A web build is three files — the page, its JS and the module — and
|
||||||
|
the page is the one named here: emcc derives the other two from it,
|
||||||
|
and it is the one a browser opens. *)
|
||||||
(match target with
|
(match target with
|
||||||
|
| Some t when Flan.Build.is_web t -> base ^ ".html"
|
||||||
| Some t when String.starts_with ~prefix:"wasm32" t -> base ^ ".wasm"
|
| Some t when String.starts_with ~prefix:"wasm32" t -> base ^ ".wasm"
|
||||||
| _ -> base)
|
| _ -> base)
|
||||||
| _ ->
|
| _ ->
|
||||||
prerr_endline
|
prerr_endline
|
||||||
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
||||||
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi]";
|
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web]";
|
||||||
exit 2
|
exit 2
|
||||||
in
|
in
|
||||||
with_errors path (fun () ->
|
with_errors path (fun () ->
|
||||||
@ -285,7 +290,7 @@ let () =
|
|||||||
prerr_endline
|
prerr_endline
|
||||||
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
|
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
|
||||||
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
||||||
[--debug] [--sanitize] [--target=wasm32-wasi]\n\
|
[--debug] [--sanitize] [--target=wasm32-wasi|web]\n\
|
||||||
\ flan run <file.flan> [args...]\n\
|
\ flan run <file.flan> [args...]\n\
|
||||||
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
|
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
|
||||||
\ flan dev <program.flan> [-s socket]";
|
\ flan dev <program.flan> [-s socket]";
|
||||||
|
|||||||
321
lib/build.ml
321
lib/build.ml
@ -4,12 +4,24 @@
|
|||||||
|
|
||||||
{v flan → typed IR → .ll → clang --target={native,wasm32} v}
|
{v flan → typed IR → .ll → clang --target={native,wasm32} v}
|
||||||
|
|
||||||
|
Three targets now, and the third takes a different driver: the browser is
|
||||||
|
built by emcc, which accepts the same .ll. See [is_web].
|
||||||
|
|
||||||
Not the dev path — that one never invokes the clang driver, because the
|
Not the dev path — that one never invokes the clang driver, because the
|
||||||
driver *is* the cost, and goes llc + ld -shared + dlopen instead. That is
|
driver *is* the cost, and goes llc + ld -shared + dlopen instead. That is
|
||||||
[shared], at the bottom of this file, measured at ~19ms. *)
|
[shared], at the bottom of this file, measured at ~19ms. *)
|
||||||
|
|
||||||
let clang = try Sys.getenv "FLAN_CLANG" with Not_found -> "clang"
|
let clang = try Sys.getenv "FLAN_CLANG" with Not_found -> "clang"
|
||||||
|
|
||||||
|
(* The browser target's compiler. emcc is a clang driver with a sysroot, a
|
||||||
|
builtins archive, a JS runtime and an HTML shell already attached, so the
|
||||||
|
whole of [wasm_sysroot], [wasm_builtins] and [wasm_resource_dir] below —
|
||||||
|
everything the wasi target has to find by hand — is what emcc *is*. It also
|
||||||
|
accepts a .ll on its command line, which is the one thing that had to be
|
||||||
|
true for this target to exist at all: [Emit] writes IR text and nothing
|
||||||
|
else. *)
|
||||||
|
let emcc = try Sys.getenv "FLAN_EMCC" with Not_found -> "emcc"
|
||||||
|
|
||||||
let write path contents =
|
let write path contents =
|
||||||
let ch = open_out path in
|
let ch = open_out path in
|
||||||
output_string ch contents;
|
output_string ch contents;
|
||||||
@ -50,7 +62,10 @@ let cachedir () =
|
|||||||
d
|
d
|
||||||
|
|
||||||
type opts = {
|
type opts = {
|
||||||
target : string option; (* None is the host; "wasm32-wasi" is the other *)
|
(* None is the host. "wasm32-wasi" is the headless one, run by a WASI
|
||||||
|
runtime; "web" is the browser one, built by emscripten. They share the
|
||||||
|
word wasm and almost nothing else — see [is_wasi] and [is_web]. *)
|
||||||
|
target : string option;
|
||||||
opt : string;
|
opt : string;
|
||||||
keep : bool; (* leave the .ll behind *)
|
keep : bool; (* leave the .ll behind *)
|
||||||
checks : bool; (* bounds-check [at] and [slice] *)
|
checks : bool; (* bounds-check [at] and [slice] *)
|
||||||
@ -134,11 +149,38 @@ let cflags opts =
|
|||||||
|
|
||||||
let getenv name = try Some (Sys.getenv name) with Not_found -> None
|
let getenv name = try Some (Sys.getenv name) with Not_found -> None
|
||||||
|
|
||||||
let is_wasm t = String.starts_with ~prefix:"wasm32" t
|
(* ── Two wasm targets, spelled apart ─────────────────────────────────
|
||||||
|
[web] is emscripten's: a browser, a GL context, a JS runtime, and a clang
|
||||||
|
whose sysroot and builtins come with it. [wasm32-wasi] is the headless one:
|
||||||
|
a WASI runtime, no GL, no browser, and a sysroot this file has to find.
|
||||||
|
"wasm32-unknown-emscripten" is accepted as a synonym for [web] because that
|
||||||
|
is the triple, and someone will write it.
|
||||||
|
|
||||||
|
What the two share is the machine — 32-bit pointers, no dlopen — which is
|
||||||
|
exactly the set of things [is_wasm] guards: the three refusals below are
|
||||||
|
about the machine and so they apply to both. *)
|
||||||
|
let is_web t =
|
||||||
|
t = "web" || t = "emscripten"
|
||||||
|
|| String.starts_with ~prefix:"wasm32-unknown-emscripten" t
|
||||||
|
|| String.starts_with ~prefix:"wasm32-emscripten" t
|
||||||
|
|
||||||
|
let is_wasi t = String.starts_with ~prefix:"wasm32" t && not (is_web t)
|
||||||
|
let is_wasm t = is_web t || is_wasi t
|
||||||
|
|
||||||
let wasm_target opts =
|
let wasm_target opts =
|
||||||
match opts.target with Some t when is_wasm t -> true | _ -> false
|
match opts.target with Some t when is_wasm t -> true | _ -> false
|
||||||
|
|
||||||
|
let web_target opts =
|
||||||
|
match opts.target with Some t when is_web t -> true | _ -> false
|
||||||
|
|
||||||
|
let wasi_target opts =
|
||||||
|
match opts.target with Some t when is_wasi t -> true | _ -> false
|
||||||
|
|
||||||
|
(* Which compiler a target is built by. This is not a flag difference: emcc is
|
||||||
|
a different program with a different driver, and the object cache key below
|
||||||
|
carries it for the same reason it carries clang's mtime. *)
|
||||||
|
let compiler opts = if web_target opts then emcc else clang
|
||||||
|
|
||||||
(* Where on PATH a program is, or None. *)
|
(* Where on PATH a program is, or None. *)
|
||||||
let on_path prog =
|
let on_path prog =
|
||||||
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
||||||
@ -256,6 +298,19 @@ let wasm_resource_dir () =
|
|||||||
let target_flags opts =
|
let target_flags opts =
|
||||||
match opts.target with
|
match opts.target with
|
||||||
| None -> []
|
| None -> []
|
||||||
|
(* The browser target adds no flags to a compile at all: emcc already is the
|
||||||
|
triple, the sysroot and the builtins. What it does need is to exist, and
|
||||||
|
an emcc that is not there must be refused here — where the reason can name
|
||||||
|
it — and not at the first "command not found" from a subshell. *)
|
||||||
|
| Some t when is_web t ->
|
||||||
|
if on_path emcc = None && not (Sys.file_exists emcc) then
|
||||||
|
failwith
|
||||||
|
(Printf.sprintf
|
||||||
|
"web: no %s on PATH. The browser target is built by emscripten: \
|
||||||
|
install an emsdk and source its emsdk_env.sh, or set FLAN_EMCC to \
|
||||||
|
the emcc to use."
|
||||||
|
emcc);
|
||||||
|
[]
|
||||||
| Some t when not (is_wasm t) -> [ "--target=" ^ t ]
|
| Some t when not (is_wasm t) -> [ "--target=" ^ t ]
|
||||||
| Some t ->
|
| Some t ->
|
||||||
let sysroot = wasm_sysroot () in
|
let sysroot = wasm_sysroot () in
|
||||||
@ -285,23 +340,202 @@ let wasm_main_source =
|
|||||||
"int flan_entry(int argc, char **argv) __asm__(\"main\");\n\
|
"int flan_entry(int argc, char **argv) __asm__(\"main\");\n\
|
||||||
int __main_argc_argv(int argc, char **argv) { return flan_entry(argc, argv); }\n"
|
int __main_argc_argv(int argc, char **argv) { return flan_entry(argc, argv); }\n"
|
||||||
|
|
||||||
|
(* ── The browser: the main loop, the shell, and the link ─────────────
|
||||||
|
The mechanism, and why it is not the one the old note predicted.
|
||||||
|
|
||||||
|
BUILT.md says a web build "drives the loop with [emscripten_set_main_loop]
|
||||||
|
instead of a [while]. That is a different [main], not a different program."
|
||||||
|
The first half is right about the browser and wrong about what it costs
|
||||||
|
here: [emscripten_set_main_loop] wants the loop body as a callback, so every
|
||||||
|
example that spells
|
||||||
|
|
||||||
|
(until (rl/window-should-close?) ...)
|
||||||
|
|
||||||
|
would have to be cut in half — an init and a tick — by hand, in every file,
|
||||||
|
and the resulting program would no longer be the one that runs natively.
|
||||||
|
|
||||||
|
[-sASYNCIFY] is the other half of the same browser fact and costs none of
|
||||||
|
that. It rewrites the module so a call can suspend and resume across a
|
||||||
|
return to the event loop, and raylib's web platform is built for it:
|
||||||
|
[WindowShouldClose] on PLATFORM_WEB is an [emscripten_sleep(16)] that then
|
||||||
|
returns false (raylib 5.5, platforms/rcore_web.c). So the Flan [until] loop
|
||||||
|
yields to the browser once a frame, at the call it already makes, and not
|
||||||
|
one example changes. Same program, same [main], same source on both targets.
|
||||||
|
|
||||||
|
What it costs is real and worth writing down: asyncify instruments the whole
|
||||||
|
module, which is roughly a doubling of code size and a measurable slowdown
|
||||||
|
on the instrumented paths. It is applied to every web build rather than to
|
||||||
|
the ones that block, because "does this program block" is not a question
|
||||||
|
this file can answer and a flag set that varies per program is a cache key
|
||||||
|
that varies per program. *)
|
||||||
|
|
||||||
|
let web_shell_default =
|
||||||
|
{html|<!doctype html>
|
||||||
|
<!-- The smallest page that makes a Flan web build openable: a canvas for
|
||||||
|
raylib's GL context, and stdout on the page rather than only in the
|
||||||
|
console, because a headless check and a human reader want the same text.
|
||||||
|
emcc substitutes the module's JS for {{{ SCRIPT }}}; nothing else here is
|
||||||
|
emscripten's. Replace it with FLAN_WEB_SHELL. -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>flan</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; background: #14161a; color: #d8dee9;
|
||||||
|
font: 13px/1.5 ui-monospace, monospace; }
|
||||||
|
#canvas { display: block; margin: 0 auto; background: #000;
|
||||||
|
outline: none; }
|
||||||
|
#out { white-space: pre-wrap; padding: 8px 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas" tabindex="-1"
|
||||||
|
oncontextmenu="event.preventDefault()"></canvas>
|
||||||
|
<pre id="out"></pre>
|
||||||
|
<script>
|
||||||
|
var out = document.getElementById('out');
|
||||||
|
var Module = {
|
||||||
|
canvas: document.getElementById('canvas'),
|
||||||
|
print: function (t) { out.textContent += t + '\n'; },
|
||||||
|
printErr: function (t) { out.textContent += t + '\n'; },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{{{ SCRIPT }}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|html}
|
||||||
|
|
||||||
|
(* The shell is a string here rather than a file in the tree for the same
|
||||||
|
reason [Runtime_src] is: it has to be present wherever the compiler is, and
|
||||||
|
a build that cannot find its own shell is a build that fails for a reason
|
||||||
|
nobody spelled. FLAN_WEB_SHELL replaces it. *)
|
||||||
|
let web_shell_file () =
|
||||||
|
match getenv "FLAN_WEB_SHELL" with
|
||||||
|
| Some p when Sys.file_exists p -> p
|
||||||
|
| Some p ->
|
||||||
|
failwith (Printf.sprintf "web: FLAN_WEB_SHELL is %s, which does not exist" p)
|
||||||
|
| None ->
|
||||||
|
let p = Filename.concat (workdir ()) "flan-shell.html" in
|
||||||
|
write p web_shell_default;
|
||||||
|
p
|
||||||
|
|
||||||
|
(* The flags the browser target adds at the link, and only at the link.
|
||||||
|
|
||||||
|
ALLOW_MEMORY_GROWTH because the default heap is 16MB and a texture is not
|
||||||
|
small. EXPORT_ES6=0 and the default MODULARIZE are left alone so that the
|
||||||
|
.js is a plain script both the shell and node can run — which is what makes
|
||||||
|
the test headless.
|
||||||
|
|
||||||
|
--shell-file only when the output is a page. Asking emcc for a .js and
|
||||||
|
handing it an HTML shell is accepted and ignored, which is the kind of
|
||||||
|
silence this file tries not to produce. *)
|
||||||
|
let web_link_flags ~out =
|
||||||
|
[ "-sASYNCIFY"; "-sALLOW_MEMORY_GROWTH=1" ]
|
||||||
|
@ (if Filename.extension out = ".html" then
|
||||||
|
[ "--shell-file"; Filename.quote (web_shell_file ()) ]
|
||||||
|
else [])
|
||||||
|
|
||||||
|
(* ── A [link] line may be addressed to one target ────────────────────
|
||||||
|
`vendor/raylib/link` names Fedora's libraylib.so.550, which exists on the
|
||||||
|
host and nowhere else; the browser wants a static archive built by
|
||||||
|
emscripten and three -s flags besides. So a line may carry a tag:
|
||||||
|
|
||||||
|
@native -l:libraylib.so.550
|
||||||
|
@web ${FLAN_RAYLIB_WEB}
|
||||||
|
|
||||||
|
and an untagged line applies to every target, which is what every existing
|
||||||
|
`link` file is.
|
||||||
|
|
||||||
|
The selection happens *here* and not in [Load], which is where the file is
|
||||||
|
read, because [Load] resolves imports before a target is chosen — the same
|
||||||
|
program is built for both — and a package's linker arguments are carried to
|
||||||
|
this function as a flat list of strings. [Load] passing the lines through
|
||||||
|
untouched is the whole of its part in this.
|
||||||
|
|
||||||
|
${NAME} expands from the environment. An unset one is refused by name: the
|
||||||
|
archive a web build needs is built once by vendor/raylib/build-web.sh and
|
||||||
|
lives at a path only that machine knows, and the alternative to naming it
|
||||||
|
here is a linker error about GLFW symbols. *)
|
||||||
|
|
||||||
|
let link_tags = [ "native"; "wasi"; "web" ]
|
||||||
|
|
||||||
|
let target_tag opts =
|
||||||
|
if web_target opts then "web"
|
||||||
|
else if wasi_target opts then "wasi"
|
||||||
|
else "native"
|
||||||
|
|
||||||
|
(* ${NAME} → the environment's NAME. Nothing else is substituted: this is not a
|
||||||
|
shell, and a linker argument containing a $ that is not a ${ is left alone
|
||||||
|
rather than guessed at. *)
|
||||||
|
let expand_vars ~where s =
|
||||||
|
let b = Buffer.create (String.length s) in
|
||||||
|
let n = String.length s in
|
||||||
|
let rec go i =
|
||||||
|
if i >= n then ()
|
||||||
|
else if i + 1 < n && s.[i] = '$' && s.[i + 1] = '{' then
|
||||||
|
match String.index_from_opt s (i + 2) '}' with
|
||||||
|
| None -> Buffer.add_char b s.[i]; go (i + 1)
|
||||||
|
| Some j ->
|
||||||
|
let name = String.sub s (i + 2) (j - i - 2) in
|
||||||
|
(match getenv name with
|
||||||
|
| Some v -> Buffer.add_string b v
|
||||||
|
| None ->
|
||||||
|
failwith
|
||||||
|
(Printf.sprintf
|
||||||
|
"%s: the linker argument %s wants %s, which is not set in the \
|
||||||
|
environment"
|
||||||
|
where s name));
|
||||||
|
go (j + 1)
|
||||||
|
else (Buffer.add_char b s.[i]; go (i + 1))
|
||||||
|
in
|
||||||
|
go 0;
|
||||||
|
Buffer.contents b
|
||||||
|
|
||||||
|
let select_lflags opts flags =
|
||||||
|
let want = target_tag opts in
|
||||||
|
List.filter_map
|
||||||
|
(fun f ->
|
||||||
|
if String.length f > 0 && f.[0] = '@' then begin
|
||||||
|
let tag, rest =
|
||||||
|
match String.index_opt f ' ' with
|
||||||
|
| Some i ->
|
||||||
|
(String.sub f 1 (i - 1),
|
||||||
|
String.trim (String.sub f (i + 1) (String.length f - i - 1)))
|
||||||
|
| None -> (String.sub f 1 (String.length f - 1), "")
|
||||||
|
in
|
||||||
|
if not (List.mem tag link_tags) then
|
||||||
|
failwith
|
||||||
|
(Printf.sprintf
|
||||||
|
"link: @%s is not a target — the tags are %s, and an untagged \
|
||||||
|
line applies to all of them"
|
||||||
|
tag
|
||||||
|
(String.concat ", " (List.map (fun t -> "@" ^ t) link_tags)));
|
||||||
|
if tag = want && rest <> "" then
|
||||||
|
Some (expand_vars ~where:("link: @" ^ tag) rest)
|
||||||
|
else None
|
||||||
|
end
|
||||||
|
else Some (expand_vars ~where:"link" f))
|
||||||
|
flags
|
||||||
|
|
||||||
(* What the compiler itself is, cheaply: its path, size and mtime. A clang
|
(* What the compiler itself is, cheaply: its path, size and mtime. A clang
|
||||||
upgrade changes one of those, so the key changes with it — without paying a
|
upgrade changes one of those, so the key changes with it — without paying a
|
||||||
[clang --version] subprocess on every build, which would cost most of what
|
[clang --version] subprocess on every build, which would cost most of what
|
||||||
the cache buys. *)
|
the cache buys. Taken of whichever compiler the target uses, so an emcc
|
||||||
let clang_stamp =
|
object and a clang object of the same source cannot collide. *)
|
||||||
lazy
|
let stamp_of prog =
|
||||||
(let path =
|
let path =
|
||||||
if Filename.is_relative clang then
|
if Filename.is_relative prog then
|
||||||
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
let dirs = String.split_on_char ':' (try Sys.getenv "PATH" with Not_found -> "") in
|
||||||
(try List.find (fun d -> Sys.file_exists (Filename.concat d clang))
|
(try List.find (fun d -> Sys.file_exists (Filename.concat d prog))
|
||||||
dirs |> fun d -> Filename.concat d clang
|
dirs |> fun d -> Filename.concat d prog
|
||||||
with Not_found -> clang)
|
with Not_found -> prog)
|
||||||
else clang
|
else prog
|
||||||
in
|
in
|
||||||
match Unix.stat path with
|
match Unix.stat path with
|
||||||
| st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime
|
| st -> Printf.sprintf "%s:%d:%f" path st.Unix.st_size st.Unix.st_mtime
|
||||||
| exception Unix.Unix_error _ -> path)
|
| exception Unix.Unix_error _ -> path
|
||||||
|
|
||||||
|
let clang_stamp = lazy (stamp_of clang)
|
||||||
|
|
||||||
(* Compile one C translation unit to an object file, reusing a cached one when
|
(* Compile one C translation unit to an object file, reusing a cached one when
|
||||||
the source text, the compiler and the flags are all unchanged. The key has
|
the source text, the compiler and the flags are all unchanged. The key has
|
||||||
@ -312,11 +546,12 @@ let compile_c ~opts ?tflags ~src ~name () =
|
|||||||
resource directory decide which headers and which builtins an object was
|
resource directory decide which headers and which builtins an object was
|
||||||
built against, so repointing either must not serve a stale .o. *)
|
built against, so repointing either must not serve a stale .o. *)
|
||||||
let tflags = match tflags with Some f -> f | None -> target_flags opts in
|
let tflags = match tflags with Some f -> f | None -> target_flags opts in
|
||||||
|
let cc = compiler opts in
|
||||||
let key =
|
let key =
|
||||||
Digest.to_hex
|
Digest.to_hex
|
||||||
(Digest.string
|
(Digest.string
|
||||||
(String.concat "\000"
|
(String.concat "\000"
|
||||||
[ name; src; Lazy.force clang_stamp; opts.opt;
|
[ name; src; stamp_of cc; opts.opt;
|
||||||
String.concat " " (cflags opts);
|
String.concat " " (cflags opts);
|
||||||
String.concat " " tflags ]))
|
String.concat " " tflags ]))
|
||||||
in
|
in
|
||||||
@ -330,14 +565,14 @@ let compile_c ~opts ?tflags ~src ~name () =
|
|||||||
let tmp = Printf.sprintf "%s.%d.tmp" obj (Unix.getpid ()) in
|
let tmp = Printf.sprintf "%s.%d.tmp" obj (Unix.getpid ()) in
|
||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt ]
|
([ Filename.quote cc; opts.opt ]
|
||||||
@ cflags opts
|
@ cflags opts
|
||||||
@ [ "-c" ] @ tflags
|
@ [ "-c" ] @ tflags
|
||||||
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
||||||
in
|
in
|
||||||
let code = Sys.command cmd in
|
let code = Sys.command cmd in
|
||||||
if code <> 0 then
|
if code <> 0 then
|
||||||
failwith (Printf.sprintf "%s failed (exit %d) on %s" clang code name);
|
failwith (Printf.sprintf "%s failed (exit %d) on %s" cc code name);
|
||||||
(try Unix.rename tmp obj with Unix.Unix_error _ -> ());
|
(try Unix.rename tmp obj with Unix.Unix_error _ -> ());
|
||||||
(try Sys.remove c with Sys_error _ -> ())
|
(try Sys.remove c with Sys_error _ -> ())
|
||||||
end;
|
end;
|
||||||
@ -351,10 +586,15 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
(* A dev build is the REPL's, and the REPL reaches a running process through
|
(* A dev build is the REPL's, and the REPL reaches a running process through
|
||||||
[-rdynamic] and [dlopen]. Neither exists on wasm32, so the combination is
|
[-rdynamic] and [dlopen]. Neither exists on wasm32, so the combination is
|
||||||
refused rather than quietly producing a module nothing can attach to. *)
|
refused rather than quietly producing a module nothing can attach to. *)
|
||||||
|
(* Named for the target that was asked for, since two of them answer to
|
||||||
|
[wasm_target] now and "wasm32:" on a --target=web build reads as a
|
||||||
|
compiler that did not hear the question. *)
|
||||||
|
let tname = if web_target opts then "web" else "wasm32" in
|
||||||
if wasm_target opts && opts.dev then
|
if wasm_target opts && opts.dev then
|
||||||
failwith
|
failwith
|
||||||
"wasm32: --dev is native only — the reload path is dlopen, which wasm32 \
|
(tname
|
||||||
has no equivalent of";
|
^ ": --dev is native only — the reload path is dlopen, which wasm has \
|
||||||
|
no equivalent of");
|
||||||
(* Refused rather than emitted-and-hoped-for. The member offsets in the DWARF
|
(* Refused rather than emitted-and-hoped-for. The member offsets in the DWARF
|
||||||
are computed for the host's layout — [ptr] 8 bytes — and wasm32's pointer
|
are computed for the host's layout — [ptr] 8 bytes — and wasm32's pointer
|
||||||
is 4, so a slice's [len] is at byte 8 there and at byte 16 here. Emitting
|
is 4, so a slice's [len] is at byte 8 there and at byte 16 here. Emitting
|
||||||
@ -363,16 +603,26 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
keeps meeting at the FFI boundary. *)
|
keeps meeting at the FFI boundary. *)
|
||||||
if wasm_target opts && opts.debug then
|
if wasm_target opts && opts.debug then
|
||||||
failwith
|
failwith
|
||||||
"wasm32: --debug is native only — the DWARF member offsets are computed \
|
(tname
|
||||||
for the host's layout, and wasm32's 32-bit pointer moves every one of \
|
^ ": --debug is native only — the DWARF member offsets are computed \
|
||||||
them";
|
for the host's layout, and wasm32's 32-bit pointer moves every one \
|
||||||
|
of them");
|
||||||
(* There is no wasm32 sanitizer runtime to link against: clang accepts
|
(* There is no wasm32 sanitizer runtime to link against: clang accepts
|
||||||
-fsanitize=address for the triple and the link fails on
|
-fsanitize=address for the triple and the link fails on
|
||||||
__asan_report_load4. Refused by name rather than met at the linker. *)
|
__asan_report_load4. Refused by name rather than met at the linker. *)
|
||||||
|
(* emscripten does ship an ASan, so the web half of this refusal is weaker
|
||||||
|
than the wasi half: it is untested here rather than known to be
|
||||||
|
impossible. Refused all the same, because a sanitizer that has never been
|
||||||
|
run is a sanitizer whose silence means nothing, and the sweep this project
|
||||||
|
runs (@sanitize) is native. *)
|
||||||
if wasm_target opts && opts.sanitize then
|
if wasm_target opts && opts.sanitize then
|
||||||
failwith
|
failwith
|
||||||
"wasm32: --sanitize is native only — there is no libclang_rt.asan for \
|
(if web_target opts then
|
||||||
wasm32-wasi to link against";
|
"web: --sanitize is native only — emscripten has its own ASan, and \
|
||||||
|
nothing here has ever run it; the sanitizer sweep is the native one"
|
||||||
|
else
|
||||||
|
"wasm32: --sanitize is native only — there is no libclang_rt.asan for \
|
||||||
|
wasm32-wasi to link against");
|
||||||
(* -O0 is not a choice a debug build offers: [llvm.dbg.declare] describes an
|
(* -O0 is not a choice a debug build offers: [llvm.dbg.declare] describes an
|
||||||
alloca, and at -O2 mem2reg deletes the alloca. [sanitize] deliberately
|
alloca, and at -O2 mem2reg deletes the alloca. [sanitize] deliberately
|
||||||
does not do this: see [opts]. *)
|
does not do this: see [opts]. *)
|
||||||
@ -399,8 +649,11 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
let objs =
|
let objs =
|
||||||
cc Runtime_src.source "flan_rt.c"
|
cc Runtime_src.source "flan_rt.c"
|
||||||
:: [ cc Runtime_src.dev_source "flan_dev.c" ]
|
:: [ cc Runtime_src.dev_source "flan_dev.c" ]
|
||||||
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source]. *)
|
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source].
|
||||||
@ (if wasm_target opts then [ cc wasm_main_source "flan_wasm_main.c" ] else [])
|
Not the browser's: emscripten's start code calls [main] under that name,
|
||||||
|
so the .ll's @main is already the entry point and the shim would be a
|
||||||
|
second definition of it. *)
|
||||||
|
@ (if wasi_target opts then [ cc wasm_main_source "flan_wasm_main.c" ] else [])
|
||||||
(* The generated half of the FFI: one translation unit holding a typedef
|
(* The generated half of the FFI: one translation unit holding a typedef
|
||||||
per struct that crosses and a wrapper per (declare-c ...), compiled
|
per struct that crosses and a wrapper per (declare-c ...), compiled
|
||||||
exactly like a package's hand-written .c. It rides on the program
|
exactly like a package's hand-written .c. It rides on the program
|
||||||
@ -414,7 +667,7 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
in
|
in
|
||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
([ Filename.quote (compiler opts); opts.opt; "-Wno-override-module" ]
|
||||||
(* -g at the link so clang does not strip, and keeps the object files'
|
(* -g at the link so clang does not strip, and keeps the object files'
|
||||||
debug sections; the .ll carries its own. The sanitizer flags have to
|
debug sections; the .ll carries its own. The sanitizer flags have to
|
||||||
be here too — they are what pulls in libclang_rt.asan and the UBSan
|
be here too — they are what pulls in libclang_rt.asan and the UBSan
|
||||||
@ -424,9 +677,12 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
@ cflags opts
|
@ cflags opts
|
||||||
@ (if opts.dev then [ "-rdynamic" ] else [])
|
@ (if opts.dev then [ "-rdynamic" ] else [])
|
||||||
@ tflags
|
@ tflags
|
||||||
|
@ (if web_target opts then web_link_flags ~out else [])
|
||||||
@ [ Filename.quote ll ]
|
@ [ Filename.quote ll ]
|
||||||
@ List.map Filename.quote objs
|
@ List.map Filename.quote objs
|
||||||
@ lflags
|
(* A package's linker arguments, with the lines addressed to another
|
||||||
|
target dropped and ${VAR} expanded. See [select_lflags]. *)
|
||||||
|
@ select_lflags opts lflags
|
||||||
(* The prelude declares sqrtf, so every link needs libm. It goes here
|
(* The prelude declares sqrtf, so every link needs libm. It goes here
|
||||||
and not in the leading flags: the default --as-needed drops a
|
and not in the leading flags: the default --as-needed drops a
|
||||||
library named before the object that wants it, so at -O2 this would
|
library named before the object that wants it, so at -O2 this would
|
||||||
@ -480,8 +736,9 @@ let shared ?(opts = default) ~ir ~out () : timing =
|
|||||||
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
|
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
|
||||||
if wasm_target opts then
|
if wasm_target opts then
|
||||||
failwith
|
failwith
|
||||||
"wasm32: the reload path is native only — it is llc + ld -shared + \
|
((if web_target opts then "web" else "wasm32")
|
||||||
dlopen, and wasm32 has no dlopen";
|
^ ": the reload path is native only — it is llc + ld -shared + dlopen, \
|
||||||
|
and wasm has no dlopen");
|
||||||
let dir = workdir () in
|
let dir = workdir () in
|
||||||
let base = Filename.remove_extension (Filename.basename out) in
|
let base = Filename.remove_extension (Filename.basename out) in
|
||||||
let ll = Filename.concat dir (base ^ ".ll") in
|
let ll = Filename.concat dir (base ^ ".ll") in
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user