Skip to content

Commit 9022349

Browse files
committed
escriptize: AOT precompilation and trailer embedding
Add --aot <target> and --jit_beams options: every beam in the packed AVM is compiled to native code with AtomVM's jit_precompile (beams that already carry an avmN chunk, e.g. a precompiled atomvmlib, are kept as is), and the AVM is repacked with the start module preserved. Add --embed trailer|objcopy and default to trailer: the AVM is appended to the AtomVM binary followed by a 16-byte trailer (8-byte little-endian size + ATOMVMv1 magic). This needs no external tooling and, unlike objcopy, survives macOS code signature checks. Also fix packbeam_api:create options to use start_module: the start key was silently ignored and the entrypoint flag only survived by being carried over from the input AVM.
1 parent ad82c28 commit 9022349

2 files changed

Lines changed: 182 additions & 7 deletions

File tree

src/atomvm_escriptize_provider.erl

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,28 @@
3333
{atomvmlib, $l, "atomvmlib", string, "Path to atomvmlib.avm (default: auto discovered)"},
3434
{output, $o, "output", string, "Output executable name (default: app name)"},
3535
{objcopy, $c, "objcopy", string, "Path to objcopy tool (auto-detected if not specified)"},
36-
{start, $s, "start", atom, "Start module (default: app name)"}
36+
{start, $s, "start", atom, "Start module (default: app name)"},
37+
{aot, $a, "aot", string,
38+
"AOT-compile beams to native code for the given JIT target (e.g. aarch64)"},
39+
{jit_beams, $j, "jit_beams", string,
40+
"Path to the AtomVM JIT compiler beams (required with --aot)"},
41+
{embed, $e, "embed", string,
42+
"AVM embedding mechanism: trailer (default) or objcopy"}
3743
]).
3844

3945
-define(DEFAULT_OPTS, #{
4046
atomvm_binary => undefined,
4147
atomvmlib => undefined,
4248
output => undefined,
4349
objcopy => undefined,
44-
start => undefined
50+
start => undefined,
51+
aot => undefined,
52+
jit_beams => undefined,
53+
embed => "trailer"
4554
}).
4655

56+
-define(ATOMVM_TRAILER_MAGIC, <<"ATOMVMv1">>).
57+
4758
%%
4859
%% provider implementation
4960
%%
@@ -88,7 +99,6 @@ do(State) ->
8899
TargetAVM = filename:join(DirName, Name ++ ".avm"),
89100
AtomVMLib = get_atomvmlib_path(Opts),
90101
AtomVMBinary = get_atomvm_binary(Opts),
91-
ObjCopyTool = get_objcopy_tool(Opts),
92102
OutputExe = get_output_path(Opts, DirName, Name),
93103

94104
% Get start module (default to app name)
@@ -99,13 +109,24 @@ do(State) ->
99109
end,
100110

101111
% Create packed AVM with atomvmlib
102-
PackedAVM = create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule),
112+
PackedAVM0 = create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule),
113+
114+
% Optionally AOT-compile the packed beams to native code
115+
PackedAVM = maybe_aot_precompile(PackedAVM0, Opts, DirName, Name, StartModule),
103116

104117
% Copy AtomVM binary
105118
ok = copy_atomvm_binary(AtomVMBinary, OutputExe),
106119

107120
% Embed AVM into executable
108-
ok = embed_avm(ObjCopyTool, OutputExe, PackedAVM),
121+
case maps:get(embed, Opts) of
122+
"objcopy" ->
123+
ObjCopyTool = get_objcopy_tool(Opts),
124+
ok = embed_avm(ObjCopyTool, OutputExe, PackedAVM);
125+
"trailer" ->
126+
ok = embed_avm_trailer(OutputExe, PackedAVM);
127+
OtherEmbed ->
128+
throw({unknown_embed_mechanism, OtherEmbed})
129+
end,
109130

110131
% Make executable
111132
ok = make_executable(OutputExe),
@@ -335,7 +356,7 @@ create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule) ->
335356
% Read both AVM files
336357
case {filelib:is_file(AtomVMLib), filelib:is_file(TargetAVM)} of
337358
{true, true} ->
338-
packbeam_api:create(PackedAVM, [AtomVMLib, TargetAVM], #{start => StartModule}),
359+
packbeam_api:create(PackedAVM, [AtomVMLib, TargetAVM], #{start_module => StartModule}),
339360
rebar_api:info("Created packed AVM: ~s with start module ~p", [PackedAVM, StartModule]),
340361
PackedAVM;
341362
{false, _} ->
@@ -344,6 +365,92 @@ create_packed_avm(TargetAVM, AtomVMLib, DirName, Name, StartModule) ->
344365
throw({file_not_found, TargetAVM})
345366
end.
346367

368+
%% @private
369+
%% AOT-compile every beam in the packed AVM to native code for the given
370+
%% JIT target, using the jit_precompile compiler from the AtomVM sources.
371+
maybe_aot_precompile(PackedAVM, Opts, DirName, Name, StartModule) ->
372+
case maps:get(aot, Opts) of
373+
undefined ->
374+
PackedAVM;
375+
Target ->
376+
JitBeams =
377+
case maps:get(jit_beams, Opts) of
378+
undefined -> throw(jit_beams_required_with_aot);
379+
Path -> Path
380+
end,
381+
case filelib:is_file(filename:join(JitBeams, "jit_precompile.beam")) of
382+
true -> ok;
383+
false -> throw({jit_precompile_not_found, JitBeams})
384+
end,
385+
true = code:add_patha(JitBeams),
386+
aot_precompile_avm(PackedAVM, Target, DirName, Name, StartModule)
387+
end.
388+
389+
%% @private
390+
aot_precompile_avm(PackedAVM, Target, DirName, Name, StartModule) ->
391+
Elements = packbeam_api:list(PackedAVM),
392+
BeamNames = [
393+
packbeam_api:get_element_name(E)
394+
|| E <- Elements, packbeam_api:is_beam(E)
395+
],
396+
WorkDir = filename:join(DirName, Name ++ "_aot"),
397+
InDir = filename:join(WorkDir, "in"),
398+
OutDir = filename:join(WorkDir, "out"),
399+
ok = filelib:ensure_dir(filename:join(InDir, "dummy")),
400+
ok = filelib:ensure_dir(filename:join(OutDir, "dummy")),
401+
rebar_api:info("AOT-compiling ~p beams for ~s", [length(BeamNames), Target]),
402+
lists:foreach(
403+
fun(E) ->
404+
case packbeam_api:is_beam(E) of
405+
true ->
406+
ElementName = packbeam_api:get_element_name(E),
407+
Data = packbeam_api:get_element_data(E),
408+
case has_native_code(Data) of
409+
true ->
410+
% already AOT-compiled (e.g. a precompiled
411+
% atomvmlib): keep as is
412+
ok = file:write_file(filename:join(OutDir, ElementName), Data);
413+
false ->
414+
BeamPath = filename:join(InDir, ElementName),
415+
ok = file:write_file(BeamPath, Data),
416+
ok = jit_precompile:compile(Target, OutDir, false, BeamPath)
417+
end;
418+
false ->
419+
ok
420+
end
421+
end,
422+
Elements
423+
),
424+
% Non-beam elements (e.g. priv files) keep their names by going through
425+
% an AVM stripped of its beams
426+
NonBeamsAVM = filename:join(WorkDir, "non_beams.avm"),
427+
packbeam_api:delete(NonBeamsAVM, PackedAVM, BeamNames),
428+
AotAVM = filename:join(DirName, Name ++ "_aot.avm"),
429+
AotBeams = [filename:join(OutDir, BeamName) || BeamName <- BeamNames],
430+
packbeam_api:create(AotAVM, AotBeams ++ [NonBeamsAVM], #{start_module => StartModule}),
431+
rebar_api:info("Created AOT packed AVM: ~s", [AotAVM]),
432+
AotAVM.
433+
434+
%% @private
435+
has_native_code(BeamData) ->
436+
case beam_lib:chunks(BeamData, ["avmN"], [allow_missing_chunks]) of
437+
{ok, {_Module, [{"avmN", Chunk}]}} -> Chunk =/= missing_chunk;
438+
_ -> false
439+
end.
440+
441+
%% @private
442+
%% Append the AVM and a 16-byte trailer (8-byte little-endian size +
443+
%% "ATOMVMv1" magic) to the executable. Unlike objcopy, this requires no
444+
%% external tooling and is tolerated by the macOS code signature checks.
445+
embed_avm_trailer(Executable, AVMFile) ->
446+
{ok, AVMData} = file:read_file(AVMFile),
447+
Size = byte_size(AVMData),
448+
Trailer = <<Size:64/little, (?ATOMVM_TRAILER_MAGIC)/binary>>,
449+
rebar_api:debug("Appending ~p byte AVM with trailer to ~s", [Size, Executable]),
450+
{ok, Fd} = file:open(Executable, [append, binary, raw]),
451+
ok = file:write(Fd, [AVMData, Trailer]),
452+
ok = file:close(Fd).
453+
347454
%% @private
348455
copy_atomvm_binary(Source, Dest) ->
349456
rebar_api:debug("Copying AtomVM binary from ~s to ~s", [Source, Dest]),

test/driver/src/escriptize_tests.erl

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
run(Opts) ->
2525
ok = test_defaults(Opts),
26+
ok = test_aot(Opts),
2627
ok.
2728

2829
%% @private
@@ -44,7 +45,74 @@ test_defaults(Opts) ->
4445
ExecPath = test:make_path([AppDir, "_build/default/bin/myscript"]),
4546
ok = test:file_exists(ExecPath),
4647

47-
[] = test:execute_cmd(ExecPath, Opts),
48+
% stderr carries VM diagnostics (e.g. JIT compilation timing); the
49+
% escript contract is about stdout
50+
[] = test:execute_cmd(ExecPath ++ " 2>/dev/null", Opts),
51+
52+
% The default (trailer) mechanism appends the AVM followed by a
53+
% 16-byte trailer: 8-byte little-endian size + "ATOMVMv1" magic
54+
{ok, Exec} = file:read_file(ExecPath),
55+
TrailerOffset = byte_size(Exec) - 16,
56+
<<_:TrailerOffset/binary, AVMSize:64/little, "ATOMVMv1">> = Exec,
57+
true = AVMSize > 0 andalso AVMSize < TrailerOffset,
58+
59+
test:tick().
60+
61+
%% @private
62+
%% AOT precompilation needs the jit compiler beams from an AtomVM source
63+
%% tree and an AtomVM binary built with JIT support; the test is skipped
64+
%% unless ATOMVM_REBAR3_PLUGIN_TEST_JIT_BEAMS, _JIT_TARGET (and optionally
65+
%% ATOMVM_REBAR3_PLUGIN_TEST_ATOMVM/_ATOMVMLIB) are set.
66+
test_aot(Opts) ->
67+
case
68+
{
69+
os:getenv("ATOMVM_REBAR3_PLUGIN_TEST_JIT_BEAMS"),
70+
os:getenv("ATOMVM_REBAR3_PLUGIN_TEST_JIT_TARGET")
71+
}
72+
of
73+
{JitBeams, Target} when JitBeams =/= false, Target =/= false ->
74+
test_aot(Opts, JitBeams, Target);
75+
_ ->
76+
io:format("(aot test skipped: ATOMVM_REBAR3_PLUGIN_TEST_JIT_* unset) "),
77+
ok
78+
end.
79+
80+
%% @private
81+
test_aot(Opts, JitBeams, Target) ->
82+
AppsDir = maps:get(apps_dir, Opts),
83+
AppDir = test:make_path([AppsDir, "myscript"]),
84+
85+
CmdOpts0 = [
86+
{"--aot", Target},
87+
{"--jit_beams", JitBeams},
88+
{"--output", "myscript_aot"}
89+
],
90+
CmdOpts1 =
91+
case os:getenv("ATOMVM_REBAR3_PLUGIN_TEST_ATOMVM") of
92+
false -> CmdOpts0;
93+
AtomVM -> [{"--atomvm_binary", AtomVM} | CmdOpts0]
94+
end,
95+
CmdOpts =
96+
case os:getenv("ATOMVM_REBAR3_PLUGIN_TEST_ATOMVMLIB") of
97+
false -> CmdOpts1;
98+
AtomVMLib -> [{"--atomvmlib", AtomVMLib} | CmdOpts1]
99+
end,
100+
101+
Cmd = create_escriptize_cmd(AppDir, CmdOpts, []),
102+
Output = test:execute_cmd(Cmd, Opts),
103+
test:debug(Output, Opts),
104+
105+
ok = test:expect_contains("AOT-compiling", Output),
106+
ok = test:expect_contains("Created AOT packed AVM:", Output),
107+
ok = test:expect_contains("Created standalone executable:", Output),
108+
109+
ExecPath = test:make_path([AppDir, "_build/default/bin/myscript_aot"]),
110+
ok = test:file_exists(ExecPath),
111+
112+
Run = test:execute_cmd(ExecPath ++ " 2>&1 >/dev/null", Opts),
113+
% AOT-compiled modules must not be compiled again at runtime
114+
false = string:find(Run, "Compilation of") =/= nomatch,
115+
[] = test:execute_cmd(ExecPath ++ " 2>/dev/null", Opts),
48116

49117
test:tick().
50118

0 commit comments

Comments
 (0)