blob: 74153384bb3c175616081151aa072ae7c4b5640b [file]
"""Compiles a wasm module into a linkable prelinked-AOT pair: an object file
whose text the native linker resolves and places in the host image, plus a
generated cpp embedding the .aot metadata container and registering both with
the runtime's prelinked registry.
The object and the container come from the same wamrc invocation flags, and
the tool refuses to emit anything unless the container's text payload is byte
identical to the object's .text, which is the property the whole scheme
rests on.
"""
import argparse
import re
import struct
import subprocess
import sys
from pathlib import Path
AOT_SECTION_TEXT = 2
# --xip implies --disable-llvm-intrinsics: floating point lowers to
# aot_intrinsic_* helper calls instead of llvm.experimental.constrained.*
# nodes.
WAMRC_FLAGS = [
"--xip",
"--bounds-checks=1",
"--target=aarch64v8",
"--target-abi=gnu",
# x18 is the console's reserved platform register, and the gnu triple
# would otherwise let LLVM allocate it freely. Features require an
# explicit cpu; generic keeps codegen comparable across bakes.
"--cpu=generic",
"--cpu-features=+reserve-x18",
"--opt-level=1",
]
def run(cmd):
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
sys.exit(f"command failed: {' '.join(str(c) for c in cmd)}\n"
f"{result.stdout}{result.stderr}")
return result
def extract_wasm_from_riv(data: bytes) -> bytes:
# Validation-lane heuristic; the product path is rive prelink, which
# decodes the riv through the runtime and takes the module bytes from
# the object model. A file with several embedded modules is ambiguous
# here, so refuse it rather than hash the wrong span.
start = data.find(b"\x00asm")
if start < 0:
sys.exit("no wasm module found in riv")
if data.find(b"\x00asm", start + 4) != -1:
sys.exit("multiple wasm magics in riv; extract the module "
"authoritatively and rerun on the .wasm")
i = start + 8
def uleb(pos):
value = 0
shift = 0
while True:
byte = data[pos]
pos += 1
value |= (byte & 0x7F) << shift
if not byte & 0x80:
return value, pos
shift += 7
while i < len(data):
try:
section_id, j = uleb(i)
size, j = uleb(j)
except IndexError:
break
if section_id > 13 or j + size > len(data):
break
# Trailing riv bytes can pass as a section; a custom section's
# name must fit and read as UTF-8, or the module ended already.
if section_id == 0:
try:
name_len, k = uleb(j)
data[k:k + name_len].decode("utf-8")
except (IndexError, UnicodeDecodeError):
break
if k + name_len > j + size:
break
i = j + size
return data[start:i]
def aot_text_payload(aot: bytes) -> bytes:
p = 8
while True:
p = (p + 3) & ~3
if p + 8 > len(aot):
break
section_type, size = struct.unpack_from("<II", aot, p)
p += 8
if section_type == AOT_SECTION_TEXT:
body = aot[p:p + size]
literal_size = struct.unpack_from("<I", body, 0)[0]
if literal_size != 0:
sys.exit(f"unexpected literal size {literal_size}; the "
"prelinked loader assumes bare code")
return body[4:]
p += size
sys.exit("no text section in aot container")
def strip_text_payload(aot: bytes) -> bytes:
"""Re-serializes the container with an empty text section: the linked
object supplies that code, so embedding it again would ship every module
twice. The runtime walker substitutes the linked text for section 2 and
never reads the container's copy."""
out = bytearray(aot[:8])
p = 8
while True:
p = (p + 3) & ~3
if p + 8 > len(aot):
break
section_type, size = struct.unpack_from("<II", aot, p)
p += 8
body = aot[p:p + size]
p += size
while len(out) % 4:
out.append(0)
if section_type == AOT_SECTION_TEXT:
out += struct.pack("<II", section_type, 0)
else:
out += struct.pack("<II", section_type, size) + body
return bytes(out)
def fnv1a64(data: bytes) -> int:
value = 0xCBF29CE484222325
for byte in data:
value = ((value ^ byte) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
return value
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--name", required=True,
help="identifier for symbols and file names")
parser.add_argument("--riv", type=Path, required=True)
parser.add_argument("--wamrc", type=Path, required=True)
parser.add_argument("--objcopy", type=Path, required=True)
parser.add_argument("--out-dir", type=Path, required=True)
args = parser.parse_args()
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", args.name):
sys.exit(f"--name '{args.name}' is not a valid identifier component")
args.out_dir.mkdir(parents=True, exist_ok=True)
wasm = extract_wasm_from_riv(args.riv.read_bytes())
wasm_path = args.out_dir / f"{args.name}.wasm"
wasm_path.write_bytes(wasm)
object_path = args.out_dir / f"{args.name}.o"
aot_path = args.out_dir / f"{args.name}.aot"
run([args.wamrc, *WAMRC_FLAGS, "--format=object",
"-o", object_path, wasm_path])
run([args.wamrc, *WAMRC_FLAGS, "-o", aot_path, wasm_path])
text_bin = args.out_dir / f"{args.name}.text.bin"
run([args.objcopy, "-O", "binary", "--only-section=.text",
object_path, text_bin])
object_text = text_bin.read_bytes()
container_text = aot_text_payload(aot_path.read_bytes())
if object_text != container_text:
sys.exit(f"{args.name}: object .text ({len(object_text)} bytes) != "
f"container text ({len(container_text)} bytes); "
"wamrc emitted divergent code")
# Locals cannot collide across modules; the one global marks the text
# start for the generated registration.
text_symbol = f"rive_prelinked_{args.name}_text"
run([args.objcopy, "-w", "--localize-symbol=*", object_path])
run([args.objcopy, f"--add-symbol={text_symbol}=.text:0,global",
object_path])
key = fnv1a64(wasm)
aot = strip_text_payload(aot_path.read_bytes())
lines = []
for i in range(0, len(aot), 16):
chunk = ", ".join(f"0x{b:02x}" for b in aot[i:i + 16])
lines.append(f" {chunk},")
array = "\n".join(lines)
cpp = f"""/*
* Generated by gen_prelinked_aot.py; do not edit.
* module: {args.name} key: {key:016x} text: {len(container_text)} bytes
*/
#include "rive/wasm/prelinked_aot.hpp"
extern "C" const uint8_t {text_symbol}[];
namespace
{{
alignas(8) const uint8_t kAotContainer[] = {{
{array}
}};
const bool kRegistered = []() {{
rive::registerPrelinkedAotModule({{
0x{key:016x}ull,
kAotContainer,
sizeof(kAotContainer),
{text_symbol},
{len(container_text)},
{len(wasm)},
}});
return true;
}}();
}} // namespace
"""
cpp_path = args.out_dir / f"{args.name}_prelinked.cpp"
cpp_path.write_text(cpp)
print(f"{args.name}: key {key:016x}, text {len(container_text)} bytes, "
f"metadata {len(aot)} bytes -> {cpp_path.name}, {object_path.name}")
if __name__ == "__main__":
main()