diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 9e7dd87d26..132050103e 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -535,6 +535,19 @@ jobs: ulimit -c unlimited ./tests/test-heap + - name: "Test: test-jit_stream_flash with valgrind" + if: matrix.library-arch == '' + working-directory: build + run: | + ulimit -c unlimited + valgrind --error-exitcode=1 ./tests/test-jit_stream_flash + + - name: "Test: test-jit_stream_flash" + working-directory: build + run: | + ulimit -c unlimited + ./tests/test-jit_stream_flash + - name: "Test: test-mailbox with valgrind" if: matrix.library-arch == '' working-directory: build diff --git a/doc/src/atomvm-internals.md b/doc/src/atomvm-internals.md index 60e2919b02..62ad888a71 100644 --- a/doc/src/atomvm-internals.md +++ b/doc/src/atomvm-internals.md @@ -137,7 +137,7 @@ Following BEAM, there are two flavors of the emulator: jit and emu, but eventual - Native: the VM only runs native code and all code must be precompiled on the desktop using the JIT compiler (which effectively is a AOT or Ahead-of-Time compiler). In this mode, it is not necessary to bundle the jit compiler on the embedded target. - Hybrid: the VM can run native code as well as emulated BEAM code and some code is precompiled on the desktop. -JIT is available on some platforms (currently only x86_64 and aarch64) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted. +JIT is available on some platforms (currently only x86_64, aarch64 and armv6m) and compiles Erlang bytecode at runtime. Erlang bytecode is never interpreted. EMU is available on all platforms and Erlang bytecode is interpreted. Modules can include precompiled code in a dedicated beam chunk with name 'avmN'. The chunk can contain native code for several architectures, however it may only contain native code for a given version of the native interface. Current version is 1. This native code is executed by the jit-flavor of the emulator as well as the emu flavor if execution of precompiled is enabled. @@ -158,6 +158,27 @@ A backend implementation is required for each architecture. The backend is calle A stream implementation is responsible for streaming the machine code, especially in the context of low memory. Two implementations currently exist: `jit_stream_binary` that streams assembly code to an Erlang binary, suitable for tests and precompilation on the desktop, and `jit_stream_mmap` that streams assembly code in an `mmap(2)` allocated page, suitable for JIT compilation on Unix. +### Embedded JIT and Native + +On embedded devices, Native mode means the code is precompiled on the desktop and executed natively on the device. This currently works on all ARMv6M devices (Pico and STM32). + +The default partition scheme on all platforms is optimized for the Emulated VM which is larger than the JIT or Native VM, and for the Emulated atomvmlib (with no native code for estdlib and no jit library) which is smaller than the JIT atomvmlib (that includes native code for estdlib and jit library). + +JIT mode means the Erlang bytecode is compiled to native code directly on the device. This actually is possible on Raspberry Pi Pico by using the flash to store the native code. The first time the code is executed, it is compiled and streamed to flash, and for next runs (including at a future boot), the native code is directly executed. + +To achive embedded JIT, it is required to flash the device with the JIT compiler for armv6m which is part of the jit library. This library is quite large, so for Pico boards that come with 2MB of flash, it is required to remove jit modules for other backends. It is also required to change the way code is partitioned. + +For example, it is possible to have the following offsets defined in `src/platforms/rp2/src/main.c`: + +``` +#define LIB_AVM ((void *) 0x10060000) +#define MAIN_AVM ((void *) 0x101B0000) +``` + +To fit in the lib partition, all networking modules should also be removed (the Pico doesn't have any networking capacity). + +After the first run, compiled modules in flash are used unless there is a version mismatch or the application avm or the library avm have been updated on the device. AVM packages end with a section called "end" (0x656E64). When the JIT compiler flashes native code, it changes this name to "END" (0x454E44), by effectively clearing 3 bits in the flash, which is possible without erasing any flash block. Any rewrite of these avm packages will overwrite the section names to "end". + ## The Scheduler In SMP builds, AtomVM runs one scheduler thread per core. Scheduler threads are actually started on demand. The number of scheduler threads can be queried with [`erlang:system_info/1`](./apidocs/erlang/estdlib/erlang.md#system_info1) and be modified with [`erlang:system_flag/2`](./apidocs/erlang/estdlib/erlang.md#system_flag2). All scheduler threads are considered equal and there is no notion of main thread except when shutting down (main thread is shut down last). diff --git a/libs/jit/src/jit_armv6m.erl b/libs/jit/src/jit_armv6m.erl index 8b9b45b19e..f38b906aac 100644 --- a/libs/jit/src/jit_armv6m.erl +++ b/libs/jit/src/jit_armv6m.erl @@ -78,7 +78,8 @@ -include("primitives.hrl"). -include("term.hrl"). --define(ASSERT(Expr), true = Expr). +%-define(ASSERT(Expr), true = Expr). +-define(ASSERT(Expr), ok). %% ARMv6-M AAPCS32 ABI: r0-r3 are used for argument passing and return value. %% r0-r1 form a double-word for 64-bit returns, additional args passed on stack. @@ -395,7 +396,7 @@ jump_table0( I3 = jit_armv6m_asm:add(pc, r3), I4 = jit_armv6m_asm:nop(), - JumpEntry = <>, + JumpEntry = <>, Stream1 = StreamModule:append(Stream0, JumpEntry), % Add relocation for the data entry so update_branches/2 can patch the jump target @@ -850,7 +851,7 @@ branch_to_label_code(State, Offset, Label, {Label, LabelOffset}) -> branch_to_label_code( #state{available_regs = [TempReg | _], branches = Branches} = State0, Offset, Label, false ) -> - {CodeBlock, SequenceSize} = + SequenceSize = if Offset rem 4 =:= 0 -> % Aligned @@ -862,7 +863,7 @@ branch_to_label_code( % Placeholder offset I5 = <<0:32/little>>, Seq = <>, - {Seq, byte_size(Seq)}; + byte_size(Seq); true -> % Unaligned I1 = jit_armv6m_asm:ldr(TempReg, {pc, 4}), @@ -871,16 +872,17 @@ branch_to_label_code( % Placeholder offset I4 = <<0:32/little>>, Seq = <>, - {Seq, byte_size(Seq)} + byte_size(Seq) end, % Add relocation entry + CodeBlock = binary:copy(<<16#FF>>, SequenceSize), Reloc = {Label, Offset, {far_branch, SequenceSize, TempReg}}, State1 = State0#state{branches = [Reloc | Branches]}, {State1, CodeBlock}; branch_to_label_code( #state{available_regs = [], branches = Branches} = State0, Offset, Label, false ) -> - {CodeBlock, SequenceSize} = + SequenceSize = if Offset rem 4 =/= 0 -> % Unaligned @@ -898,7 +900,7 @@ branch_to_label_code( Seq = <>, - {Seq, byte_size(Seq)}; + byte_size(Seq); true -> % Aligned I1 = jit_armv6m_asm:push([r0]), @@ -912,9 +914,10 @@ branch_to_label_code( I7 = <<0:32/little>>, Seq = <>, - {Seq, byte_size(Seq)} + byte_size(Seq) end, % Add relocation entry + CodeBlock = binary:copy(<<16#FF>>, SequenceSize), Reloc = {Label, Offset, {far_branch, SequenceSize, ?IP_REG}}, State1 = State0#state{branches = [Reloc | Branches]}, {State1, CodeBlock}; @@ -998,7 +1001,8 @@ if_else_block( Stream2 = State2#state.stream, %% Emit unconditional branch to skip the else block (will be replaced) ElseJumpOffset = StreamModule:offset(Stream2), - ElseJumpInstr = jit_armv6m_asm:b(0), + ?ASSERT(byte_size(jit_armv6m_asm:b(0)) =:= 2), + ElseJumpInstr = <<16#FFFF:16>>, Stream3 = StreamModule:append(Stream2, ElseJumpInstr), %% Else block starts here. OffsetAfter = StreamModule:offset(Stream3), @@ -1072,24 +1076,22 @@ if_block_cond(#state{stream_module = StreamModule, stream = Stream0} = State0, { %% Compare register with 0 I1 = jit_armv6m_asm:cmp(Reg, 0), %% Branch if positive (N flag clear) - I2 = jit_armv6m_asm:bcc(pl, 0), - Stream1 = StreamModule:append(Stream0, <>), + CC = pl, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(pl, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = State0#state{stream = Stream1}, - {State1, pl, byte_size(I1)}; + {State1, CC, byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0} = State0, {Reg, '<', Val} ) when is_atom(Reg), is_integer(Val), Val >= 0, Val =< 255 -> I1 = jit_armv6m_asm:cmp(Reg, Val), % ge = greater than or equal - I2 = jit_armv6m_asm:bcc(ge, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream1 = StreamModule:append(Stream0, Code), + CC = ge, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = State0#state{stream = Stream1}, - {State1, ge, byte_size(I1)}; + {State1, CC, byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0, available_regs = [Temp | _]} = State0, {Reg, '<', Val} @@ -1100,14 +1102,11 @@ if_block_cond( Offset1 = StreamModule:offset(Stream1), I1 = jit_armv6m_asm:cmp(Reg, Temp), % ge = greater than or equal - I2 = jit_armv6m_asm:bcc(ge, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream2 = StreamModule:append(Stream1, Code), + CC = ge, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream2 = StreamModule:append(Stream1, <>), State2 = State1#state{stream = Stream2}, - {State2, ge, Offset1 - Offset0 + byte_size(I1)}; + {State2, CC, Offset1 - Offset0 + byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0} = State0, {RegOrTuple, '<', RegB} @@ -1119,15 +1118,12 @@ if_block_cond( end, I1 = jit_armv6m_asm:cmp(Reg, RegB), % ge = greater than or equal - I2 = jit_armv6m_asm:bcc(ge, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream1 = StreamModule:append(Stream0, Code), + CC = ge, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, ge, byte_size(I1)}; + {State2, CC, byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0} = State0, {RegOrTuple, '==', 0} ) -> @@ -1139,11 +1135,12 @@ if_block_cond( %% Compare register with 0 I1 = jit_armv6m_asm:cmp(Reg, 0), %% Branch if not equal - I2 = jit_armv6m_asm:bcc(ne, 0), - Stream1 = StreamModule:append(Stream0, <>), + CC = ne, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, ne, byte_size(I1)}; + {State2, CC, byte_size(I1)}; %% Delegate (int) forms to regular forms since we only have 32-bit words if_block_cond(State, {'(int)', RegOrTuple, '==', 0}) -> if_block_cond(State, {RegOrTuple, '==', 0}); @@ -1159,15 +1156,12 @@ if_block_cond( RegOrTuple -> RegOrTuple end, I1 = jit_armv6m_asm:cmp(Reg, Val), - I2 = jit_armv6m_asm:bcc(eq, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream1 = StreamModule:append(Stream0, Code), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, eq, byte_size(I1)}; + {State2, CC, byte_size(I1)}; if_block_cond(State, {'(int)', RegOrTuple, '!=', Val}) when is_integer(Val) -> if_block_cond(State, {RegOrTuple, '!=', Val}); if_block_cond( @@ -1180,28 +1174,25 @@ if_block_cond( RegOrTuple -> RegOrTuple end, I1 = jit_armv6m_asm:cmp(Reg, Val), - I2 = jit_armv6m_asm:bcc(ne, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream1 = StreamModule:append(Stream0, Code), + CC = ne, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, ne, byte_size(I1)}; + {State2, CC, byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0} = State0, {{free, RegA}, '==', {free, RegB}} ) -> % Compare two free registers: cmp RegA, RegB; beq I1 = jit_armv6m_asm:cmp(RegA, RegB), - Stream1 = StreamModule:append(Stream0, I1), - I2 = jit_armv6m_asm:bcc(ne, 0), - Stream2 = StreamModule:append(Stream1, I2), - State1 = State0#state{stream = Stream2}, + CC = ne, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), + State1 = State0#state{stream = Stream1}, State2 = if_block_free_reg({free, RegA}, State1), State3 = if_block_free_reg({free, RegB}, State2), - {State3, ne, byte_size(I1)}; + {State3, CC, byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0, available_regs = [Temp | _]} = State0, {RegOrTuple, '==', Val} @@ -1216,15 +1207,12 @@ if_block_cond( Stream1 = State1#state.stream, Offset1 = StreamModule:offset(Stream1), I1 = jit_armv6m_asm:cmp(Reg, Temp), - I2 = jit_armv6m_asm:bcc(ne, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream2 = StreamModule:append(Stream1, Code), + CC = ne, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream2 = StreamModule:append(Stream1, <>), State2 = if_block_free_reg(RegOrTuple, State1), State3 = State2#state{stream = Stream2}, - {State3, ne, Offset1 - Offset0 + byte_size(I1)}; + {State3, CC, Offset1 - Offset0 + byte_size(I1)}; if_block_cond( #state{stream_module = StreamModule, stream = Stream0, available_regs = [Temp | _]} = State0, {RegOrTuple, '!=', Val} @@ -1239,15 +1227,12 @@ if_block_cond( Stream1 = State1#state.stream, Offset1 = StreamModule:offset(Stream1), I1 = jit_armv6m_asm:cmp(Reg, Temp), - I2 = jit_armv6m_asm:bcc(eq, 0), - Code = << - I1/binary, - I2/binary - >>, - Stream2 = StreamModule:append(Stream1, Code), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream2 = StreamModule:append(Stream1, <>), State2 = if_block_free_reg(RegOrTuple, State1), State3 = State2#state{stream = Stream2}, - {State3, eq, Offset1 - Offset0 + byte_size(I1)}; + {State3, CC, Offset1 - Offset0 + byte_size(I1)}; if_block_cond( #state{ stream_module = StreamModule, @@ -1264,12 +1249,12 @@ if_block_cond( % Test bit 0: shift bit 0 to MSB and branch if positive (bit was 0/false) I1 = jit_armv6m_asm:lsls(Temp, Reg, 31), % branch if negative (bit was 1/true) - I2 = jit_armv6m_asm:bcc(mi, 0), - Code = <>, - Stream1 = StreamModule:append(Stream0, Code), + CC = mi, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, mi, byte_size(I1)}; + {State2, CC, byte_size(I1)}; if_block_cond( #state{ stream_module = StreamModule, @@ -1286,12 +1271,12 @@ if_block_cond( % Test bit 0: shift bit 0 to MSB and branch if negative (bit was 1/true) I1 = jit_armv6m_asm:lsls(Temp, Reg, 31), % branch if positive (bit was 0/false) - I2 = jit_armv6m_asm:bcc(pl, 0), - Code = <>, - Stream1 = StreamModule:append(Stream0, Code), + CC = pl, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, - {State2, pl, byte_size(I1)}; + {State2, CC, byte_size(I1)}; if_block_cond( #state{ stream_module = StreamModule, @@ -1320,8 +1305,8 @@ if_block_cond( TestCode1 = jit_armv6m_asm:tst(Reg, Temp), {<>, eq} end, - I2 = jit_armv6m_asm:bcc(BranchCond, 0), - Code = <>, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(BranchCond, 0)) =:= 2), + Code = <>, Stream1 = StreamModule:append(Stream0, Code), State1 = if_block_free_reg(RegOrTuple, State0), State2 = State1#state{stream = Stream1}, @@ -1338,10 +1323,11 @@ if_block_cond( I1 = jit_armv6m_asm:mvns(Temp, Reg), % 32 - 4 I2 = jit_armv6m_asm:lsls(Temp, Temp, 28), - I3 = jit_armv6m_asm:bcc(eq, 0), - Stream1 = StreamModule:append(Stream0, <>), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = State0#state{stream = Stream1}, - {State1, eq, byte_size(I1) + byte_size(I2)}; + {State1, CC, byte_size(I1) + byte_size(I2)}; if_block_cond( #state{ stream_module = StreamModule, @@ -1353,11 +1339,12 @@ if_block_cond( I1 = jit_armv6m_asm:mvns(Reg, Reg), % 32 - 4 I2 = jit_armv6m_asm:lsls(Reg, Reg, 28), - I3 = jit_armv6m_asm:bcc(eq, 0), - Stream1 = StreamModule:append(Stream0, <>), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream1 = StreamModule:append(Stream0, <>), State1 = State0#state{stream = Stream1}, State2 = if_block_free_reg(RegTuple, State1), - {State2, eq, byte_size(I1) + byte_size(I2)}; + {State2, CC, byte_size(I1) + byte_size(I2)}; if_block_cond( #state{ stream_module = StreamModule, @@ -1377,10 +1364,11 @@ if_block_cond( I2 = jit_armv6m_asm:cmp(Temp, Val), Stream3 = StreamModule:append(Stream2, I2), OffsetAfter = StreamModule:offset(Stream3), - I3 = jit_armv6m_asm:bcc(eq, 0), - Stream4 = StreamModule:append(Stream3, I3), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream4 = StreamModule:append(Stream3, <<16#FFFF:16>>), State3 = State2#state{stream = Stream4, available_regs = [Temp | State2#state.available_regs]}, - {State3, eq, OffsetAfter - OffsetBefore}; + {State3, CC, OffsetAfter - OffsetBefore}; if_block_cond( #state{ stream_module = StreamModule, @@ -1396,11 +1384,12 @@ if_block_cond( I2 = jit_armv6m_asm:cmp(Reg, Val), Stream2 = StreamModule:append(Stream1, I2), OffsetAfter = StreamModule:offset(Stream2), - I3 = jit_armv6m_asm:bcc(eq, 0), - Stream3 = StreamModule:append(Stream2, I3), + CC = eq, + ?ASSERT(byte_size(jit_armv6m_asm:bcc(CC, 0)) =:= 2), + Stream3 = StreamModule:append(Stream2, <<16#FFFF:16>>), State3 = State1#state{stream = Stream3}, State4 = if_block_free_reg(RegTuple, State3), - {State4, eq, OffsetAfter - OffsetBefore}. + {State4, CC, OffsetAfter - OffsetBefore}. -spec if_block_free_reg(armv6m_register() | {free, armv6m_register()}, state()) -> state(). if_block_free_reg({free, Reg}, State0) -> @@ -1855,7 +1844,7 @@ set_registers_args0( set_registers_args0( State, [Arg | ArgsT], [_ArgReg | ArgsRegs], [?CTX_REG | ParamRegs], AvailGP, StackOffset ) -> - false = lists:member(?CTX_REG, ArgsRegs), + ?ASSERT(not lists:member(?CTX_REG, ArgsRegs)), State1 = set_registers_args1(State, Arg, ?CTX_REG, StackOffset), set_registers_args0(State1, ArgsT, ArgsRegs, ParamRegs, AvailGP, StackOffset); set_registers_args0( @@ -2520,7 +2509,8 @@ set_continuation_to_offset( ) -> OffsetRef = make_ref(), Offset = StreamModule:offset(Stream0), - I1 = jit_armv6m_asm:adr(Temp, 4), + ?ASSERT(byte_size(jit_armv6m_asm:adr(Temp, 4)) =:= 2), + I1 = <<16#FFFF:16>>, Reloc = {OffsetRef, Offset, {adr, Temp}}, % Set thumb bit (LSB = 1) by adding 1 to the 4-byte aligned address I2 = jit_armv6m_asm:adds(Temp, Temp, 1), @@ -2925,10 +2915,12 @@ decrement_reductions_and_maybe_schedule_next( Stream1 = StreamModule:append(Stream0, <>), BNEOffset = StreamModule:offset(Stream1), % Branch if reduction count is not zero - I4 = jit_armv6m_asm:bcc(ne, 0), + ?ASSERT(byte_size(jit_armv6m_asm:bcc(ne, 0)) =:= 2), + I4 = <<16#FFFF:16>>, % Set continuation to the next instruction ADROffset = BNEOffset + byte_size(I4), - I5 = jit_armv6m_asm:adr(Temp, 4), + ?ASSERT(byte_size(jit_armv6m_asm:adr(Temp, 4) =:= 2)), + I5 = <<16#FFFF:16>>, I6 = jit_armv6m_asm:adds(Temp, Temp, 1), I7 = jit_armv6m_asm:str(Temp, ?JITSTATE_CONTINUATION(TempJitState)), % Append the instructions to the stream @@ -3061,8 +3053,8 @@ set_cp(State0) -> Offset = StreamModule:offset(Stream0), % build cp with module_index << 24 I1 = jit_armv6m_asm:lsls(Reg, Reg, 24), - % Emit a single nop as placeholder for offset load instruction - I2 = jit_armv6m_asm:nop(), + % Placeholder for offset load instruction + I2 = <<16#FFFF:16>>, MOVOffset = Offset + byte_size(I1), % OR the module index with the offset (loaded in temp register) I3 = jit_armv6m_asm:orrs(Reg, TempReg), diff --git a/src/libAtomVM/avmpack.c b/src/libAtomVM/avmpack.c index 075dc94966..624fa82493 100644 --- a/src/libAtomVM/avmpack.c +++ b/src/libAtomVM/avmpack.c @@ -35,6 +35,7 @@ static inline int pad(int size) bool avmpack_is_valid(const void *avmpack_binary, uint32_t size) { + // "#!/usr/bin/env AtomVM" const unsigned char pack_header[AVMPACK_SIZE] = { 0x23, 0x21, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, @@ -51,7 +52,7 @@ bool avmpack_is_valid(const void *avmpack_binary, uint32_t size) return memcmp(avmpack_binary, pack_header, AVMPACK_SIZE) == 0; } -int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask, const void **ptr, uint32_t *size, const char **name) +int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask, uint32_t flags_val, const void **ptr, uint32_t *size, const char **name) { int offset = AVMPACK_SIZE; const uint32_t *flags; @@ -60,7 +61,7 @@ int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask const uint32_t *sizes = ((const uint32_t *) (avmpack_binary)) + offset / sizeof(uint32_t); flags = ((const uint32_t *) (avmpack_binary)) + 1 + offset / sizeof(uint32_t); - if ((ENDIAN_SWAP_32(*flags) & flags_mask) == flags_mask) { + if ((ENDIAN_SWAP_32(*flags) & flags_mask) == flags_val) { const char *found_section_name = (const char *) (sizes + 3); int section_name_len = pad(strlen(found_section_name) + 1); diff --git a/src/libAtomVM/avmpack.h b/src/libAtomVM/avmpack.h index 85ce9562d9..a68e6e3378 100644 --- a/src/libAtomVM/avmpack.h +++ b/src/libAtomVM/avmpack.h @@ -39,6 +39,7 @@ extern "C" { #define END_OF_FILE 0 #define BEAM_START_FLAG 1 #define BEAM_CODE_FLAG 2 +#define END_OF_FILE_MASK 255 struct AVMPackData; @@ -104,12 +105,13 @@ typedef void *(*avmpack_fold_fun)(void *accum, const void *section_ptr, uint32_t * @details Finds an AVM Pack section that has certain flags set and returns a pointer to it, its size and its name. * @param avmpack_binary a pointer to valid AVM Pack file data. * @param flags_mask that will be matched against file sections. + * @param flags_value that will be matched against file sections. * @param ptr will point to the found file section. * @param size will be set to the file section size that has been found, if the section has not been found it will not be updated. * @param name the section name, as defined in the module header. * @returns 1 if the file section has been found, 0 otherwise. */ -int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask, const void **ptr, uint32_t *size, const char **name); +int avmpack_find_section_by_flag(const void *avmpack_binary, uint32_t flags_mask, uint32_t flags_value, const void **ptr, uint32_t *size, const char **name); /** * @brief Finds an AVM Pack section that has certain name. diff --git a/src/libAtomVM/jit_stream_flash.c b/src/libAtomVM/jit_stream_flash.c new file mode 100644 index 0000000000..829b7d4f76 --- /dev/null +++ b/src/libAtomVM/jit_stream_flash.c @@ -0,0 +1,928 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 by Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "jit_stream_flash.h" + +#include "avmpack.h" +#include "context.h" +#include "defaultatoms.h" +#include "erl_nif.h" +#include "erl_nif_priv.h" +#include "globalcontext.h" +#include "jit.h" +#include "module.h" +#include "nifs.h" +#include "platform_defaultatoms.h" +#include "sys.h" +#include "term.h" + +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include "trace.h" + +#define JIT_ENTRY_MAGIC 0x4A74 + +#ifdef ENABLE_TRACE +// Simple CRC32 for verification +static uint32_t crc32(const uint8_t *data, size_t len) +{ + uint32_t crc = 0xFFFFFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1)); + } + } + return ~crc; +} +#endif + +/** + * @brief JIT entry header stored in flash + * + * Each compiled module has an entry with this header followed by the native code. + */ +struct JITEntry +{ + uint16_t magic; ///< Magic number (JIT_ENTRY_MAGIC) or 0xFFFF for free space + uint16_t version; ///< Module version + uint32_t code; ///< Pointer to original BEAM code (32-bit for flash storage) + uint32_t labels; ///< Number of labels + uint32_t size; ///< Size of native code in bytes +} __attribute__((packed)); + +_Static_assert(sizeof(struct JITEntry) == 16, "sizeof(struct JITEntry) must be 16"); + +/** + * @brief JIT stream flash state + * + * Maintains the state for writing JIT code to flash with page buffering. + */ +struct JITStreamFlash +{ + struct JITEntry *jit_entry; ///< Pointer to current JIT entry in flash + uintptr_t page_base_addr; ///< Base address of current page + uint8_t page_buffer[FLASH_PAGE_SIZE]; ///< Page buffer for writing + uint8_t page_offset; ///< Current offset within page + struct JSFlashPlatformContext *pf_ctx; ///< Platform-specific context +}; + +static ErlNifResourceType *jit_stream_flash_resource_type; +static void jit_stream_flash_dtor(ErlNifEnv *caller_env, void *obj); +static bool jit_stream_flash_replace_at_addr(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr, const uint8_t *data, size_t len); + +const ErlNifResourceTypeInit jit_stream_flash_resource_type_init = { + .members = 1, + .dtor = jit_stream_flash_dtor +}; + +static struct JITEntry *jit_entry_next(struct JITEntry *jit_entry) +{ + uintptr_t next_entry_addr = ((uintptr_t) jit_entry) + sizeof(struct JITEntry) + jit_entry->size; + // Align to 4 bytes boundaries + next_entry_addr = (next_entry_addr + 3) & ~3; + + TRACE("jit_entry_next: jit_entry = %p, return %p\n", (void *) jit_entry, (void *) next_entry_addr); + + return (struct JITEntry *) next_entry_addr; +} + +/** + * @brief Check if a sector needs to be erased + * + * Scans the entire sector to check if it contains any non-0xFF bytes. + * Uses word-by-word comparison for efficiency since sectors are aligned. + * + * @param sector_addr Address of the sector (must be sector-aligned) + * @return true if sector needs erasing, false if already erased + */ +static bool jit_stream_flash_sector_needs_erase(uintptr_t sector_addr) +{ + const uintptr_t *sector_ptr = (const uintptr_t *) sector_addr; + const uintptr_t erased_pattern = ~((uintptr_t) 0); // All bits set to 1 (0xFF...FF) + size_t num_words = FLASH_SECTOR_SIZE / sizeof(uintptr_t); + + // Check if entire sector is all 0xFF by comparing word-by-word + for (size_t i = 0; i < num_words; i++) { + if (sector_ptr[i] != erased_pattern) { + return true; + } + } + + return false; +} + +static struct JITEntry *globalcontext_find_first_jit_entry(GlobalContext *global, bool *is_valid) +{ + const void *max_end_offset = NULL; + uint32_t end_size; + const void *end_offset; + const char *end_name; + bool valid_cache = true; + + struct ListHead *item; + struct ListHead *avmpack_data = synclist_rdlock(&global->avmpack_data); + LIST_FOR_EACH (item, avmpack_data) { + struct AVMPackData *avmpack_data = GET_LIST_ENTRY(item, struct AVMPackData, avmpack_head); + avmpack_find_section_by_flag(avmpack_data->data, END_OF_FILE_MASK, END_OF_FILE, &end_offset, &end_size, &end_name); + valid_cache = valid_cache && (strcmp(end_name, "END") == 0); + + if (end_offset > max_end_offset) { + max_end_offset = end_offset; + } + } + synclist_unlock(&global->avmpack_data); + + uintptr_t max_end_offset_page = ((((uintptr_t) max_end_offset) - 1) & ~(FLASH_SECTOR_SIZE - 1)); + *is_valid = valid_cache; + + TRACE("globalcontext_find_first_jit_entry: return %p\n", (void *) (max_end_offset_page + FLASH_SECTOR_SIZE)); + + return (struct JITEntry *) (max_end_offset_page + FLASH_SECTOR_SIZE); +} + +static void globalcontext_set_cache_valid(GlobalContext *global) +{ + TRACE("globalcontext_set_cache_valid\n"); + + uint32_t end_size; + const void *end_offset; + const char *end_name; + bool valid_cache; + + // Create platform context for flash operations + struct JSFlashPlatformContext *pf_ctx = jit_stream_flash_platform_init(); + if (IS_NULL_PTR(pf_ctx)) { + fprintf(stderr, "Failed to initialize platform flash context\n"); + return; + } + + do { + valid_cache = true; + struct ListHead *item; + struct ListHead *avmpack_data = synclist_rdlock(&global->avmpack_data); + LIST_FOR_EACH (item, avmpack_data) { + struct AVMPackData *avmpack_data = GET_LIST_ENTRY(item, struct AVMPackData, avmpack_head); + avmpack_find_section_by_flag(avmpack_data->data, END_OF_FILE_MASK, END_OF_FILE, &end_offset, &end_size, &end_name); + if (strcmp(end_name, "END")) { + valid_cache = false; + break; + } + } + synclist_unlock(&global->avmpack_data); + if (!valid_cache) { + // Replace "end" with "END" - this is a 3-byte string replacement + const uint8_t end_str[] = "END"; + if (!jit_stream_flash_replace_at_addr(pf_ctx, (uintptr_t) end_name, end_str, 3)) { + fprintf(stderr, "Failed to update cache validity marker from 'end' to 'END'\n"); + break; + } + } + } while (!valid_cache); + + jit_stream_flash_platform_destroy(pf_ctx); +} + +static struct JITEntry *globalcontext_find_last_jit_entry(GlobalContext *global) +{ + bool is_valid; + struct JITEntry *jit_entry = globalcontext_find_first_jit_entry(global, &is_valid); + if (!is_valid) { + TRACE("globalcontext_find_last_jit_entry, cache not valid, returning NULL\n"); + return NULL; + } + + // Find the last valid entry + struct JITEntry *last_valid = jit_entry; + while (jit_entry->magic == JIT_ENTRY_MAGIC) { + last_valid = jit_entry; + jit_entry = jit_entry_next(jit_entry); + } + TRACE("globalcontext_find_last_jit_entry, returning last valid entry at %p\n", (void *) last_valid); + return last_valid; +} + +static bool jit_stream_flash_flush_page(struct JITStreamFlash *js) +{ + // Write the page + // Note: sector is already erased by nif_jit_stream_flash_new (first sector) + // or jit_stream_flash_append (subsequent sectors when crossing boundaries) + if (!jit_stream_flash_platform_write_page(js->pf_ctx, js->page_base_addr, js->page_buffer)) { + fprintf(stderr, "Failed to write page at address 0x%lx\n", (unsigned long) js->page_base_addr); + return false; + } + + return true; +} + +static bool jit_stream_flash_finalize_entry(struct JSFlashPlatformContext *pf_ctx, struct JITEntry *jit_entry, uint16_t magic, uint16_t version, uint32_t code, uint32_t labels) +{ + uintptr_t entry_addr = (uintptr_t) jit_entry; + uint8_t page_buffer[FLASH_PAGE_SIZE]; + uintptr_t page_base_addr = entry_addr & ~(FLASH_PAGE_SIZE - 1); + + // Read current page contents + memcpy(page_buffer, (const uint8_t *) page_base_addr, FLASH_PAGE_SIZE); + + // Calculate offset within page + size_t entry_offset = entry_addr - page_base_addr; + struct JITEntry *updated_entry = (struct JITEntry *) (page_buffer + entry_offset); + + // Update fields + updated_entry->magic = magic; + updated_entry->version = version; + updated_entry->code = code; + updated_entry->labels = labels; + + // Write back to flash + if (!jit_stream_flash_platform_write_page(pf_ctx, page_base_addr, page_buffer)) { + fprintf(stderr, "Failed to finalize entry at address 0x%lx\n", (unsigned long) page_base_addr); + return false; + } + + return true; +} + +// Replace data in flash at the given absolute address +// Returns true on success, false if validation fails (trying to set bits 0→1) +static bool jit_stream_flash_replace_at_addr(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr, const uint8_t *data, size_t len) +{ + uintptr_t replace_start = addr; + uintptr_t replace_end = replace_start + len; + + // Iterate over all pages that need to be updated + uintptr_t current_page_addr = replace_start & ~(FLASH_PAGE_SIZE - 1); + size_t data_offset = 0; + + while (current_page_addr < replace_end) { + // Calculate the range within this page that needs to be replaced + uintptr_t page_start_offset = 0; + uintptr_t page_end_offset = FLASH_PAGE_SIZE; + + if (current_page_addr < replace_start) { + page_start_offset = replace_start - current_page_addr; + } + + if (current_page_addr + FLASH_PAGE_SIZE > replace_end) { + page_end_offset = replace_end - current_page_addr; + } + + size_t copy_len = page_end_offset - page_start_offset; + + // Prepare page buffer + uint8_t page_buffer[FLASH_PAGE_SIZE]; + uintptr_t page_base_addr = current_page_addr; + + // Read current page contents + memcpy(page_buffer, (const uint8_t *) page_base_addr, FLASH_PAGE_SIZE); + + // Verify that we're only clearing bits (1→0), not setting them (0→1) + const uint8_t *flash_ptr = (const uint8_t *) page_base_addr; + for (size_t i = 0; i < copy_len; i++) { + uint8_t flash_byte = flash_ptr[page_start_offset + i]; + uint8_t new_byte = data[data_offset + i]; + + // Check if we're trying to set any bits (0→1) + if ((new_byte & ~flash_byte) != 0) { + fprintf(stderr, "\n=== FLASH REPLACE VALIDATION FAILED ===\n"); + fprintf(stderr, "Attempting to set bits (0→1) without erase!\n"); + fprintf(stderr, "Page address: 0x%lx\n", (unsigned long) page_base_addr); + fprintf(stderr, "Offset in page: %zu, flash byte: 0x%02hhx, new byte: 0x%02hhx\n", + page_start_offset + i, flash_byte, new_byte); + fprintf(stderr, "Bits being set (0→1): 0x%02hhx\n", (new_byte & ~flash_byte)); + fprintf(stderr, "Replace address: 0x%lx, len=%zu\n", (unsigned long) addr, len); + fprintf(stderr, "========================================\n\n"); + return false; + } + } + + // Update with new data + memcpy(page_buffer + page_start_offset, data + data_offset, copy_len); + + // Write back to flash + if (!jit_stream_flash_platform_write_page(pf_ctx, page_base_addr, page_buffer)) { + fprintf(stderr, "Failed to replace data at address 0x%lx\n", (unsigned long) page_base_addr); + return false; + } + + data_offset += copy_len; + current_page_addr += FLASH_PAGE_SIZE; + } + + return true; +} + +static bool jit_stream_flash_append(struct JITStreamFlash *js, const uint8_t *buffer, size_t count) +{ + while (count > 0) { + // Validate flash constraints: can only write to erased (0xFF) bytes + uint8_t current_byte = js->page_buffer[js->page_offset]; + uint8_t new_byte = *buffer; + if ((~current_byte & new_byte) != 0) { + // Trying to set bits from 0→1 without erase + fprintf(stderr, "\n=== JIT STREAM FLASH APPEND ERROR ===\n"); + fprintf(stderr, "Attempting to write 0x%02x over 0x%02x at page offset %u\n", + new_byte, current_byte, js->page_offset); + fprintf(stderr, "Page base address: 0x%lx\n", (unsigned long) js->page_base_addr); + fprintf(stderr, "Flash address: 0x%lx\n", (unsigned long) (js->page_base_addr + js->page_offset)); + fprintf(stderr, "Bits being set 0→1: 0x%02x\n", (~current_byte & new_byte)); + fprintf(stderr, "This indicates the sector was not properly erased!\n"); + fprintf(stderr, "=====================================\n\n"); + return false; + } + + js->page_buffer[js->page_offset] = *buffer; + if (js->page_offset == (FLASH_PAGE_SIZE - 1)) { + if (!jit_stream_flash_flush_page(js)) { + fprintf(stderr, "jit_stream_flash_flush_page failed\n"); + return false; + } + // Move to the next page after flushing + uintptr_t previous_sector = js->page_base_addr & ~(FLASH_SECTOR_SIZE - 1); + js->page_base_addr += FLASH_PAGE_SIZE; + js->page_offset = 0; + uintptr_t new_sector = js->page_base_addr & ~(FLASH_SECTOR_SIZE - 1); + + // Check if we've entered a new sector and erase if needed + if (new_sector != previous_sector) { + if (jit_stream_flash_sector_needs_erase(new_sector)) { + TRACE("jit_stream_flash_append -- erasing new sector at %lx\n", (unsigned long) new_sector); + if (!jit_stream_flash_platform_erase_sector(js->pf_ctx, new_sector)) { + fprintf(stderr, "Failed to erase new sector at address 0x%lx\n", (unsigned long) new_sector); + return false; + } + } + } + + // Read the new page contents into the buffer + memcpy(js->page_buffer, (const uint8_t *) js->page_base_addr, FLASH_PAGE_SIZE); + } else { + js->page_offset++; + } + buffer++; + count--; + } + return true; +} + +static term nif_jit_stream_flash_new(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + struct JITEntry *last_valid_entry = globalcontext_find_last_jit_entry(ctx->global); + struct JITEntry *new_entry; + + if (last_valid_entry == NULL) { + // No valid entries, get the first position + bool is_valid; + new_entry = globalcontext_find_first_jit_entry(ctx->global, &is_valid); + } else { + // Get position after last valid entry + new_entry = jit_entry_next(last_valid_entry); + } + + // Return a resource object + struct JITStreamFlash *js = enif_alloc_resource(jit_stream_flash_resource_type, sizeof(struct JITStreamFlash)); + if (IS_NULL_PTR(js)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Initialize platform context + js->pf_ctx = jit_stream_flash_platform_init(); + if (IS_NULL_PTR(js->pf_ctx)) { + fprintf(stderr, "Failed to initialize platform flash context\n"); + enif_release_resource(js); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + js->jit_entry = new_entry; + js->page_base_addr = (uintptr_t) new_entry & ~(FLASH_PAGE_SIZE - 1); + + // Handle sector erasing for the sector where JIT entry starts + uintptr_t new_entry_addr = (uintptr_t) new_entry; + uintptr_t new_entry_sector = new_entry_addr & ~(FLASH_SECTOR_SIZE - 1); + uintptr_t sector_end = new_entry_sector + FLASH_SECTOR_SIZE; + + // Check if there's stale data from entry position to end of sector + bool has_stale_data = false; + const uintptr_t *check_ptr = (const uintptr_t *) new_entry_addr; + const uintptr_t *check_end = (const uintptr_t *) sector_end; + const uintptr_t erased_pattern = ~((uintptr_t) 0); + + while (check_ptr < check_end) { + if (*check_ptr != erased_pattern) { + has_stale_data = true; + break; + } + check_ptr++; + } + + if (has_stale_data) { + // There's stale data (from failed compilation) - need to erase but preserve data before entry + size_t preserve_size = new_entry_addr - new_entry_sector; + + if (preserve_size > 0) { + // Allocate buffer for the sector + uint8_t *sector_buffer = malloc(FLASH_SECTOR_SIZE); + if (IS_NULL_PTR(sector_buffer)) { + fprintf(stderr, "Failed to allocate sector buffer\n"); + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Copy data to preserve (before the entry) + memcpy(sector_buffer, (const uint8_t *) new_entry_sector, preserve_size); + + // Fill rest with 0xFF (erased state) + memset(sector_buffer + preserve_size, 0xFF, FLASH_SECTOR_SIZE - preserve_size); + + // Erase the sector + TRACE("nif_jit_stream_flash_new -- erasing sector with stale data at %lx (preserving %zu bytes)\n", + (unsigned long) new_entry_sector, preserve_size); + if (!jit_stream_flash_platform_erase_sector(js->pf_ctx, new_entry_sector)) { + fprintf(stderr, "Failed to erase sector with stale data\n"); + free(sector_buffer); + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(BADARG_ATOM); + } + + // Write back the preserved data page by page + for (size_t page_offset = 0; page_offset < preserve_size; page_offset += FLASH_PAGE_SIZE) { + if (!jit_stream_flash_platform_write_page(js->pf_ctx, new_entry_sector + page_offset, + sector_buffer + page_offset)) { + fprintf(stderr, "Failed to write back preserved data\n"); + free(sector_buffer); + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(BADARG_ATOM); + } + } + + free(sector_buffer); + } else { + // Entry is at sector boundary, just erase + TRACE("nif_jit_stream_flash_new -- erasing sector with stale data at %lx\n", + (unsigned long) new_entry_sector); + if (!jit_stream_flash_platform_erase_sector(js->pf_ctx, new_entry_sector)) { + fprintf(stderr, "Failed to erase sector for new JIT entry\n"); + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(BADARG_ATOM); + } + } + } else { + TRACE("nif_jit_stream_flash_new -- sector at %lx is clean (no stale data)\n", + (unsigned long) new_entry_sector); + } + + // Now handle the sector where JIT entry ends (if different from start sector) + uintptr_t entry_end = new_entry_addr + sizeof(struct JITEntry); + uintptr_t entry_end_sector = entry_end & ~(FLASH_SECTOR_SIZE - 1); + + if (entry_end_sector != new_entry_sector) { + // Entry spans two sectors - erase the end sector if needed + if (jit_stream_flash_sector_needs_erase(entry_end_sector)) { + TRACE("nif_jit_stream_flash_new -- erasing end sector at %lx\n", + (unsigned long) entry_end_sector); + if (!jit_stream_flash_platform_erase_sector(js->pf_ctx, entry_end_sector)) { + fprintf(stderr, "Failed to erase end sector for new JIT entry\n"); + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(BADARG_ATOM); + } + } + } + + memcpy(js->page_buffer, (const uint8_t *) js->page_base_addr, FLASH_PAGE_SIZE); + js->page_offset = (uintptr_t) new_entry & (FLASH_PAGE_SIZE - 1); + + TRACE("nif_jit_stream_flash_new entry is %p, page_offset is %lx\n", (void *) new_entry, (unsigned long) js->page_offset); + + // Append the first bytes, which may flush the page + struct JITEntry header; + header.magic = 0xFFFF; + header.version = 0xFFFF; + header.code = 0xFFFFFFFF; + header.labels = 0xFFFFFFFF; + header.size = 0xFFFFFFFF; + if (!jit_stream_flash_append(js, (const uint8_t *) &header, sizeof(header))) { + jit_stream_flash_platform_destroy(js->pf_ctx); + enif_release_resource(js); + RAISE_ERROR(BADARG_ATOM); + } + + term obj = enif_make_resource(erl_nif_env_from_context(ctx), js); + enif_release_resource(js); // decrement refcount after enif_alloc_resource + return obj; +} + +static term nif_jit_stream_flash_offset(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], jit_stream_flash_resource_type, &js_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + uintptr_t current_addr = js_obj->page_base_addr + js_obj->page_offset; + uintptr_t base_addr = ((uintptr_t) js_obj->jit_entry + sizeof(struct JITEntry)); + + int offset = current_addr - base_addr; + + return term_from_int(offset); +} + +static term nif_jit_stream_flash_append(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[1], term_is_binary); + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], jit_stream_flash_resource_type, &js_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + size_t binary_size = term_binary_size(argv[1]); + const uint8_t *binary_data = (const uint8_t *) term_binary_data(argv[1]); + + if (!jit_stream_flash_append(js_obj, binary_data, binary_size)) { + RAISE_ERROR(BADARG_ATOM); + } + + return argv[0]; +} + +static term nif_jit_stream_flash_replace(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_binary); + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], jit_stream_flash_resource_type, &js_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + + size_t binary_size = term_binary_size(argv[2]); + const uint8_t *binary_data = (const uint8_t *) term_binary_data(argv[2]); + avm_int_t offset = term_to_int(argv[1]); + + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + uintptr_t base_addr = ((uintptr_t) js_obj->jit_entry + sizeof(struct JITEntry)); + uintptr_t replace_start = base_addr + offset; + uintptr_t replace_end = replace_start + binary_size; + + // Iterate over all pages that need to be updated + uintptr_t current_page_addr = replace_start & ~(FLASH_PAGE_SIZE - 1); + size_t binary_offset = 0; + + while (current_page_addr < replace_end) { + // Calculate the range within this page that needs to be replaced + uintptr_t page_start_offset = 0; + uintptr_t page_end_offset = FLASH_PAGE_SIZE; + + if (current_page_addr < replace_start) { + page_start_offset = replace_start - current_page_addr; + } + + if (current_page_addr + FLASH_PAGE_SIZE > replace_end) { + page_end_offset = replace_end - current_page_addr; + } + + size_t copy_len = page_end_offset - page_start_offset; + + // Check if this is the current buffer page + if (current_page_addr == js_obj->page_base_addr) { + // Update current buffer directly + memcpy(js_obj->page_buffer + page_start_offset, binary_data + binary_offset, copy_len); + } else { + // This is an already-flushed page, need to update flash + if (!jit_stream_flash_replace_at_addr(js_obj->pf_ctx, current_page_addr + page_start_offset, + binary_data + binary_offset, + copy_len)) { + RAISE_ERROR(BADARG_ATOM); + } + } + + binary_offset += copy_len; + current_page_addr += FLASH_PAGE_SIZE; + } + + return argv[0]; +} + +static term nif_jit_stream_flash_read(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], jit_stream_flash_resource_type, &js_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + avm_int_t offset = term_to_int(argv[1]); + avm_int_t len = term_to_int(argv[2]); + + // Validate parameters + if (UNLIKELY(len <= 0 || offset < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + // Calculate current stream position + uintptr_t current_addr = js_obj->page_base_addr + js_obj->page_offset; + uintptr_t base_addr = ((uintptr_t) js_obj->jit_entry + sizeof(struct JITEntry)); + size_t stream_offset = current_addr - base_addr; + + // Check if read is within bounds + if (UNLIKELY((size_t) (offset + len) > stream_offset)) { + RAISE_ERROR(BADARG_ATOM); + } + + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BINARY_HEAP_SIZE(len), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + uintptr_t read_addr = base_addr + offset; + return term_from_literal_binary((const uint8_t *) read_addr, len, &ctx->heap, ctx->global); +} + +static term nif_jit_stream_flash_flush(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], jit_stream_flash_resource_type, &js_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + // Calculate the size BEFORE flushing + uintptr_t current_addr = js_obj->page_base_addr + js_obj->page_offset; + uintptr_t code_start = (uintptr_t) js_obj->jit_entry + sizeof(struct JITEntry); + uint32_t code_size = current_addr - code_start; + + // Check if the size field is in the current unflushed page buffer or in an already-flushed page + uintptr_t size_field_addr = (uintptr_t) &js_obj->jit_entry->size; + uintptr_t size_field_page = size_field_addr & ~(FLASH_PAGE_SIZE - 1); + + if (size_field_page == js_obj->page_base_addr) { + // Size field is in the current buffer, update it directly before flushing + size_t offset_in_page = size_field_addr - js_obj->page_base_addr; + memcpy(js_obj->page_buffer + offset_in_page, &code_size, sizeof(uint32_t)); + } else { + // Size field is in an already-flushed page, use replace + if (!jit_stream_flash_replace_at_addr(js_obj->pf_ctx, size_field_addr, + (const uint8_t *) &code_size, + sizeof(uint32_t))) { + RAISE_ERROR(BADARG_ATOM); + } + } + + // Flush the final page + if (!jit_stream_flash_flush_page(js_obj)) { + fprintf(stderr, "jit_stream_flash_flush_page failed\n"); + RAISE_ERROR(BADARG_ATOM); + } + + return argv[0]; +} + +static term nif_jit_stream_module(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + return globalcontext_make_atom(ctx->global, ATOM_STR("\x10", "jit_stream_flash")); +} + +static const struct Nif jit_stream_module_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_module +}; +static const struct Nif jit_stream_flash_new_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_new +}; +static const struct Nif jit_stream_flash_offset_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_offset +}; +static const struct Nif jit_stream_flash_append_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_append +}; +static const struct Nif jit_stream_flash_replace_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_replace +}; +static const struct Nif jit_stream_flash_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_read +}; +static const struct Nif jit_stream_flash_flush_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_jit_stream_flash_flush +}; + +ModuleNativeEntryPoint jit_stream_flash_entry_point(Context *ctx, term jit_stream) +{ + void *js_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), jit_stream, jit_stream_flash_resource_type, &js_obj_ptr))) { + return NULL; + } + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) js_obj_ptr; + + uintptr_t base_addr = ((uintptr_t) js_obj->jit_entry + sizeof(struct JITEntry)); + + // Convert to executable address (handles DBUS→IBUS, Thumb bit, etc.) + base_addr = jit_stream_flash_platform_ptr_to_executable(base_addr); + + return (ModuleNativeEntryPoint) base_addr; +} + +static void jit_stream_flash_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct JITStreamFlash *js_obj = (struct JITStreamFlash *) obj; + if (js_obj->pf_ctx) { + jit_stream_flash_platform_destroy(js_obj->pf_ctx); + } +} + +const struct Nif *jit_stream_flash_get_nif(const char *nifname) +{ + if (strcmp("jit:stream_module/0", nifname) == 0) { + return &jit_stream_module_nif; + } + if (strncmp("jit_stream_flash:", nifname, 17) == 0) { + const char *rest = nifname + 17; + if (strcmp("new/1", rest) == 0) { + return &jit_stream_flash_new_nif; + } + if (strcmp("offset/1", rest) == 0) { + return &jit_stream_flash_offset_nif; + } + if (strcmp("append/2", rest) == 0) { + return &jit_stream_flash_append_nif; + } + if (strcmp("replace/3", rest) == 0) { + return &jit_stream_flash_replace_nif; + } + if (strcmp("read/3", rest) == 0) { + return &jit_stream_flash_read_nif; + } + if (strcmp("flush/1", rest) == 0) { + return &jit_stream_flash_flush_nif; + } + } + return NULL; +} + +void jit_stream_flash_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + jit_stream_flash_resource_type = enif_init_resource_type(&env, "jit_stream_flash", &jit_stream_flash_resource_type_init, ERL_NIF_RT_CREATE, NULL); +} + +void globalcontext_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels) +{ + bool is_valid; + (void) globalcontext_find_first_jit_entry(global, &is_valid); + + struct JSFlashPlatformContext *pf_ctx = jit_stream_flash_platform_init(); + if (IS_NULL_PTR(pf_ctx)) { + fprintf(stderr, "Failed to initialize platform flash context\n"); + return; + } + + // Reverse the executable address transformation to get data address + // Platform-specific: Thumb (clear bit 0), RISC-V (IBUS→DBUS conversion) + uintptr_t data_addr = jit_stream_flash_platform_executable_to_ptr((uintptr_t) entry_point); + + struct JITEntry *jit_entry = (struct JITEntry *) (data_addr - sizeof(struct JITEntry)); + uintptr_t code = (uintptr_t) mod->code; + + // Finalize the entry + if (!jit_stream_flash_finalize_entry(pf_ctx, jit_entry, JIT_ENTRY_MAGIC, version, (uint32_t) code, labels)) { + fprintf(stderr, "jit_stream_flash_finalize_entry failed\n"); + jit_stream_flash_platform_destroy(pf_ctx); + return; + } + +#ifdef ENABLE_TRACE + // Compute CRC of entire module for verification + uint32_t module_crc = crc32((const uint8_t *) jit_entry, sizeof(struct JITEntry) + jit_entry->size); + TRACE("After finalize - jit_entry=%p CRC32=0x%08x (entry+code size=%u)\n", + (void *) jit_entry, (unsigned int) module_crc, (unsigned int) (sizeof(struct JITEntry) + jit_entry->size)); +#endif + + // Erase next sector if it's completely after the current module + struct JITEntry *current_entry = (struct JITEntry *) (data_addr - sizeof(struct JITEntry)); + struct JITEntry *next_entry = jit_entry_next(current_entry); + uintptr_t next_entry_addr = (uintptr_t) next_entry; + uintptr_t next_sector = next_entry_addr & ~(FLASH_SECTOR_SIZE - 1); + + // Calculate the sector where the current module ENDS (not where it starts) + uintptr_t current_module_end = (uintptr_t) current_entry + sizeof(struct JITEntry) + current_entry->size; + uintptr_t current_end_sector = current_module_end & ~(FLASH_SECTOR_SIZE - 1); + + // Only erase next sector if it's completely after the current module's end + // This prevents erasing a sector that contains the tail of the current module + if (next_sector > current_end_sector) { + // Next entry is in a sector completely after current module, erase it if it has stale data + if (next_entry->magic != 0xFFFF) { + TRACE("globalcontext_set_cache_native_code -- NOT erasing new sector at %lx\n", (unsigned long) next_sector); + if (!jit_stream_flash_platform_erase_sector(pf_ctx, next_sector)) { + fprintf(stderr, "jit_stream_flash_platform_erase_sector failed\n"); + jit_stream_flash_platform_destroy(pf_ctx); + return; + } + } else { + TRACE("globalcontext_set_cache_native_code -- NOT erasing new sector at %lx\n", (unsigned long) next_sector); + } + } + + if (!is_valid) { + // Mark that cache entry is valid by replacing end with END in installed AVM + globalcontext_set_cache_valid(global); + } + + jit_stream_flash_platform_destroy(pf_ctx); +} + +// Implementation of jit_stream_entry_point, sys_get_cache_native_code and +// sys_set_cache_native_code using this jit_stream +#ifndef TEST_JIT_STREAM_FLASH +ModuleNativeEntryPoint jit_stream_entry_point(Context *ctx, term jit_stream) +{ + return jit_stream_flash_entry_point(ctx, jit_stream); +} + +bool sys_get_cache_native_code(GlobalContext *global, Module *mod, uint16_t *version, ModuleNativeEntryPoint *entry_point, uint32_t *labels) +{ + bool is_valid; + struct JITEntry *jit_entry = globalcontext_find_first_jit_entry(global, &is_valid); + if (!is_valid) { + return false; + } + uintptr_t code = (uintptr_t) mod->code; + while (jit_entry->magic == JIT_ENTRY_MAGIC) { + if (jit_entry->code == (uint32_t) code) { + *version = jit_entry->version; + uintptr_t ep_addr = (uintptr_t) jit_entry + sizeof(struct JITEntry); + ep_addr = jit_stream_flash_platform_ptr_to_executable(ep_addr); + *entry_point = (ModuleNativeEntryPoint) ep_addr; + *labels = jit_entry->labels; + +#ifdef ENABLE_TRACE + // Compute CRC of entire module for verification + uint32_t module_crc = crc32((const uint8_t *) jit_entry, sizeof(struct JITEntry) + jit_entry->size); + TRACE("Loading from cache - jit_entry=%p CRC32=0x%08x (entry+code size=%u)\n", + (void *) jit_entry, (unsigned int) module_crc, (unsigned int) (sizeof(struct JITEntry) + jit_entry->size)); +#endif + + return true; + } + jit_entry = jit_entry_next(jit_entry); + } + return false; +} + +void sys_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels) +{ + globalcontext_set_cache_native_code(global, mod, version, entry_point, labels); +} +#endif diff --git a/src/libAtomVM/jit_stream_flash.h b/src/libAtomVM/jit_stream_flash.h new file mode 100644 index 0000000000..30644189ea --- /dev/null +++ b/src/libAtomVM/jit_stream_flash.h @@ -0,0 +1,129 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 by Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +/** + * @file jit_stream_flash.h + * @brief JIT code caching in flash memory - common implementation + */ + +#ifndef _JIT_STREAM_FLASH_H_ +#define _JIT_STREAM_FLASH_H_ + +#include "globalcontext.h" +#include "jit_stream_flash_platform.h" +#include "module.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Platform-specific flash context (opaque) + */ +struct JSFlashPlatformContext; + +/** + * @brief Initialize JIT stream flash subsystem + * + * @param global Global context + */ +void jit_stream_flash_init(GlobalContext *global); + +/** + * @brief Get NIF for jit_stream_flash operations + * + * @param nifname NIF name + * @return NIF pointer or NULL + */ +const struct Nif *jit_stream_flash_get_nif(const char *nifname); + +/** + * @brief Get entry point from jit_stream_flash. + * Called by `jit_stream_entry_point` + * + * @param ctx Context + * @param jit_stream JIT stream term + * @return Entry point or NULL + */ +ModuleNativeEntryPoint jit_stream_flash_entry_point(Context *ctx, term jit_stream); + +/** + * @brief Finalize flash operation by marking an entry point as valid for + * a given module. This is called by `sys_set_cache_native_code`. + * + * @param global Global context + * @param mod Module + * @param version Module version + * @param entry_point Entry point + * @param labels Number of labels + */ +void globalcontext_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels); + +/** + * @brief Initialize platform flash context + * @return Platform flash context, or NULL on error + */ +struct JSFlashPlatformContext *jit_stream_flash_platform_init(void); + +/** + * @brief Destroy platform flash context + * @param pf_ctx Platform flash context to destroy + */ +void jit_stream_flash_platform_destroy(struct JSFlashPlatformContext *pf_ctx); + +/** + * @brief Erase a flash sector at the given address + * @param pf_ctx Platform flash context + * @param addr Virtual address of the sector to erase (must be sector-aligned) + * @return true on success, false on error + */ +bool jit_stream_flash_platform_erase_sector(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr); + +/** + * @brief Write a page to flash + * @param pf_ctx Platform flash context + * @param addr Virtual address to write to (must be page-aligned) + * @param data Data to write (must be FLASH_PAGE_SIZE bytes) + * @return true on success, false on error + */ +bool jit_stream_flash_platform_write_page(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr, const uint8_t *data); + +/** + * @brief Convert data bus address to instruction bus address + * @param addr Data bus address + * @return Instruction bus address (executable pointer) + */ +uintptr_t jit_stream_flash_platform_ptr_to_executable(uintptr_t addr); + +/** + * @brief Convert instruction bus address to data bus address + * @param addr Instruction bus address (executable pointer) + * @return Data bus address + */ +uintptr_t jit_stream_flash_platform_executable_to_ptr(uintptr_t addr); + +#ifdef __cplusplus +} +#endif + +#endif // _JIT_STREAM_FLASH_H_ diff --git a/src/libAtomVM/module.c b/src/libAtomVM/module.c index 108d5027d8..64ff0569a6 100644 --- a/src/libAtomVM/module.c +++ b/src/libAtomVM/module.c @@ -353,6 +353,13 @@ Module *module_new_from_iff_binary(GlobalContext *global, const void *iff_binary fprintf(stderr, "Native code chunk found but no compatible architecture or variant found\n"); } } + } else { + ModuleNativeEntryPoint module_entry_point; + uint32_t labels; + uint16_t version; + if (sys_get_cache_native_code(global, mod, &version, &module_entry_point, &labels) && version == JIT_FORMAT_VERSION) { + module_set_native_code(mod, labels, module_entry_point); + } } #endif diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index e3d76eba41..dc87df2250 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -4806,7 +4806,7 @@ static term nif_atomvm_get_start_beam(Context *ctx, int argc, term argv[]) uint32_t size; const void *beam; const char *module_name; - if (!avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, &beam, &size, &module_name)) { + if (!avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &beam, &size, &module_name)) { synclist_unlock(&ctx->global->avmpack_data); if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -5633,6 +5633,8 @@ static term nif_code_server_set_native_code(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(argv[0], term_is_atom); VALIDATE_VALUE(argv[1], term_is_integer); + avm_int_t labels_count = term_to_int(argv[1]); + term module_name = argv[0]; Module *mod = globalcontext_get_module(ctx->global, term_to_atom_index(module_name)); if (IS_NULL_PTR(mod)) { @@ -5646,10 +5648,12 @@ static term nif_code_server_set_native_code(Context *ctx, int argc, term argv[]) SMP_MODULE_LOCK(mod); if (mod->native_code == NULL) { - module_set_native_code(mod, term_to_int(argv[1]), entry_point); + module_set_native_code(mod, labels_count, entry_point); } SMP_MODULE_UNLOCK(mod); + sys_set_cache_native_code(ctx->global, mod, JIT_FORMAT_VERSION, entry_point, labels_count); + return OK_ATOM; } #endif diff --git a/src/libAtomVM/sys.h b/src/libAtomVM/sys.h index 0735d86ed3..8ad701c3cb 100644 --- a/src/libAtomVM/sys.h +++ b/src/libAtomVM/sys.h @@ -296,6 +296,33 @@ void sys_free_platform(GlobalContext *global); */ ModuleNativeEntryPoint sys_map_native_code(const uint8_t *native_code, size_t size, size_t offset); +/** + * @brief Get the cache (typically on flash) of native code for a given module + * + * @details If module is found in cache, return a pointer to the entry point. + * Only implemented on platforms with JIT. Implementations on flash typically + * check if the jit cache is valid (for lib or for app) and use the pointer to + * code as a key. + * @param global the global context + * @param mod module to return the cache native code for + * @param version version of the cache entry (for compatibility with the VM) + * @param entry_point entry point to the module, if found + * @param labels number of labels + * @return \c true if the cache entry was found + */ +bool sys_get_cache_native_code(GlobalContext *global, Module *mod, uint16_t *version, ModuleNativeEntryPoint *entry_point, uint32_t *labels); + +/** + * @brief Add native code to cache for a given module + * + * @param global the global context + * @param mod module to add the native code for + * @param version version of the native code + * @param entry_point entry point to the module + * @param labels number of labels + */ +void sys_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels); + #ifdef __cplusplus } #endif diff --git a/src/platforms/emscripten/src/main.c b/src/platforms/emscripten/src/main.c index 27e02c3a67..c8c00dc07a 100644 --- a/src/platforms/emscripten/src/main.c +++ b/src/platforms/emscripten/src/main.c @@ -59,7 +59,7 @@ static int load_module(const char *path) const void *startup_beam = NULL; uint32_t startup_beam_size; const char *startup_module_name; - avmpack_find_section_by_flag(avmpack_data->data, 1, &startup_beam, &startup_beam_size, &startup_module_name); + avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name); if (startup_beam) { avmpack_data->in_use = true; main_module = module_new_from_iff_binary(global, startup_beam, startup_beam_size); diff --git a/src/platforms/esp32/main/main.c b/src/platforms/esp32/main/main.c index bc25c82c64..ca6d02e2f3 100644 --- a/src/platforms/esp32/main/main.c +++ b/src/platforms/esp32/main/main.c @@ -98,7 +98,7 @@ void app_main() ESP_LOGE(TAG, "Invalid startup avmpack. size=%u", size); AVM_ABORT(); } - if (!avmpack_find_section_by_flag(startup_avm, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { + if (!avmpack_find_section_by_flag(startup_avm, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { ESP_LOGE(TAG, "Error: Failed to locate start module in startup partition. (Did you flash a library by mistake?)"); AVM_ABORT(); } diff --git a/src/platforms/esp32/partitions.csv b/src/platforms/esp32/partitions.csv index 95c1cf74bc..d313cbdc81 100644 --- a/src/platforms/esp32/partitions.csv +++ b/src/platforms/esp32/partitions.csv @@ -7,6 +7,5 @@ # Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 0x1C0000, -boot.avm, data, phy, 0x1D0000, 0x40000, -main.avm, data, phy, 0x210000, 0x100000 +factory, app, factory, 0x10000, 0x160000, +main.avm, data, phy, 0x170000, 0x290000, diff --git a/src/platforms/generic_unix/lib/sys.c b/src/platforms/generic_unix/lib/sys.c index 099164dd89..eedbe060b4 100644 --- a/src/platforms/generic_unix/lib/sys.c +++ b/src/platforms/generic_unix/lib/sys.c @@ -853,4 +853,24 @@ ModuleNativeEntryPoint sys_map_native_code(const uint8_t *native_code, size_t si return (ModuleNativeEntryPoint) (native_code + offset); #endif } + +bool sys_get_cache_native_code(GlobalContext *global, Module *mod, uint16_t *version, ModuleNativeEntryPoint *entry_point, uint32_t *labels) +{ + UNUSED(global); + UNUSED(mod); + UNUSED(version); + UNUSED(entry_point); + UNUSED(labels); + return false; +} + +void sys_set_cache_native_code(GlobalContext *global, Module *mod, uint16_t version, ModuleNativeEntryPoint entry_point, uint32_t labels) +{ + UNUSED(global); + UNUSED(mod); + UNUSED(version); + UNUSED(entry_point); + UNUSED(labels); +} + #endif diff --git a/src/platforms/generic_unix/main.c b/src/platforms/generic_unix/main.c index f45fd7f14f..6908aa50f8 100644 --- a/src/platforms/generic_unix/main.c +++ b/src/platforms/generic_unix/main.c @@ -104,7 +104,7 @@ int main(int argc, char **argv) const void *startup_beam = NULL; const char *startup_module_name; uint32_t startup_beam_size; - avmpack_find_section_by_flag(avmpack_data->data, 1, &startup_beam, &startup_beam_size, &startup_module_name); + avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name); if (startup_beam) { avmpack_data->in_use = true; diff --git a/src/platforms/rp2/src/CMakeLists.txt b/src/platforms/rp2/src/CMakeLists.txt index c79433551e..f4ef67cd8f 100644 --- a/src/platforms/rp2/src/CMakeLists.txt +++ b/src/platforms/rp2/src/CMakeLists.txt @@ -55,6 +55,10 @@ else() target_compile_definitions(AtomVM PRIVATE PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=20000) endif() +if (AVM_DISABLE_SMP) + target_compile_definitions(AtomVM PRIVATE PICO_FLASH_ASSUME_CORE1_SAFE) +endif() + if (AVM_WAIT_BOOTSEL_ON_EXIT) target_compile_definitions(AtomVM PRIVATE WAIT_BOOTSEL_ON_EXIT) endif() diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index 3cc69b56a3..b9e594c9df 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -31,7 +31,6 @@ set(HEADER_FILES set(SOURCE_FILES gpiodriver.c - jit_stream_flash.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -110,4 +109,16 @@ if (PICO_CYW43_SUPPORTED) define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} gethostname "unistd.h" PRIVATE HAVE_GETHOSTNAME) endif() +if (NOT AVM_DISABLE_JIT) + target_sources( + libAtomVM${PLATFORM_LIB_SUFFIX} + PRIVATE + jit_stream_flash_platform.c + ../../../../libAtomVM/jit_stream_flash.c + jit_stream_flash_platform.h + ../../../../libAtomVM/jit_stream_flash.h + ) + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,jit_stream_flash_get_nif") +endif() + target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/jit_stream_flash_platform.c b/src/platforms/rp2/src/lib/jit_stream_flash_platform.c new file mode 100644 index 0000000000..e8a17c3537 --- /dev/null +++ b/src/platforms/rp2/src/lib/jit_stream_flash_platform.c @@ -0,0 +1,117 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 by Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef AVM_NO_JIT + +#include "jit_stream_flash.h" + +#include +#include +#include +#include + +#include "rp2_sys.h" + +// Helper structures for flash_safe_execute +struct EraseParams +{ + uintptr_t addr; +}; + +struct WriteParams +{ + uintptr_t addr; + const uint8_t *data; + size_t len; +}; + +static void __not_in_flash_func(do_erase_sector)(void *params_ptr) +{ + struct EraseParams *params = (struct EraseParams *) params_ptr; + flash_range_erase(params->addr - XIP_BASE, FLASH_SECTOR_SIZE); +} + +static void __not_in_flash_func(do_write_page)(void *params_ptr) +{ + struct WriteParams *params = (struct WriteParams *) params_ptr; + flash_range_program(params->addr - XIP_BASE, params->data, params->len); +} + +struct JSFlashPlatformContext *jit_stream_flash_platform_init(void) +{ + return (struct JSFlashPlatformContext *) 1; +} + +void jit_stream_flash_platform_destroy(struct JSFlashPlatformContext *pf_ctx) +{ + UNUSED(pf_ctx); +} + +bool jit_stream_flash_platform_erase_sector(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr) +{ + UNUSED(pf_ctx); + + struct EraseParams params = { + .addr = addr + }; + + int r = flash_safe_execute(do_erase_sector, ¶ms, UINT32_MAX); + if (UNLIKELY(r != PICO_OK)) { + fprintf(stderr, "flash_safe_execute (erase) failed with error %d\n", r); + return false; + } + + return true; +} + +bool jit_stream_flash_platform_write_page(struct JSFlashPlatformContext *pf_ctx, uintptr_t addr, const uint8_t *data) +{ + UNUSED(pf_ctx); + + struct WriteParams params = { + .addr = addr, + .data = data, + .len = FLASH_PAGE_SIZE + }; + + int r = flash_safe_execute(do_write_page, ¶ms, UINT32_MAX); + if (UNLIKELY(r != PICO_OK)) { + fprintf(stderr, "flash_safe_execute (write) failed with error %d\n", r); + return false; + } + + return true; +} + +uintptr_t jit_stream_flash_platform_ptr_to_executable(uintptr_t addr) +{ + // Set Thumb bit + return addr | 0x1; +} + +uintptr_t jit_stream_flash_platform_executable_to_ptr(uintptr_t addr) +{ + // Clear Thumb bit + return addr & ~0x1UL; +} + +REGISTER_NIF_COLLECTION(jit_stream_flash, jit_stream_flash_init, NULL, jit_stream_flash_get_nif) + +#endif // AVM_NO_JIT diff --git a/src/platforms/rp2/src/lib/jit_stream_flash_platform.h b/src/platforms/rp2/src/lib/jit_stream_flash_platform.h new file mode 100644 index 0000000000..3ee8c660bd --- /dev/null +++ b/src/platforms/rp2/src/lib/jit_stream_flash_platform.h @@ -0,0 +1,40 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 by Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef _JIT_STREAM_FLASH_PLATFORM_H_ +#define _JIT_STREAM_FLASH_PLATFORM_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// RP2040 flash constants (W25Q16JV chip) +#define FLASH_SECTOR_SIZE 4096 +#define FLASH_PAGE_SIZE 256 + +#ifdef __cplusplus +} +#endif + +#endif // _JIT_STREAM_FLASH_PLATFORM_H_ diff --git a/src/platforms/rp2/src/lib/smp.c b/src/platforms/rp2/src/lib/smp.c index 946b066305..44251e8913 100644 --- a/src/platforms/rp2/src/lib/smp.c +++ b/src/platforms/rp2/src/lib/smp.c @@ -57,14 +57,17 @@ static void scheduler_core1_entry_point(void) { _Static_assert(sizeof(uintptr_t) == sizeof(uint32_t), "Expected pointers to be 32 bits"); uint32_t ctx_int = multicore_fifo_pop_blocking(); + multicore_lockout_victim_init(); int result = scheduler_entry_point((GlobalContext *) ctx_int); UNUSED(result); + multicore_lockout_victim_deinit(); } void smp_scheduler_start(GlobalContext *ctx) { multicore_launch_core1(scheduler_core1_entry_point); multicore_fifo_push_blocking((uint32_t) ctx); + multicore_lockout_victim_init(); } bool smp_is_main_thread(GlobalContext *glb) diff --git a/src/platforms/rp2/src/main.c b/src/platforms/rp2/src/main.c index e25e1398d0..0733c85e63 100644 --- a/src/platforms/rp2/src/main.c +++ b/src/platforms/rp2/src/main.c @@ -87,9 +87,12 @@ static int app_main() if (!avmpack_is_valid(MAIN_AVM, XIP_SRAM_BASE - (uintptr_t) MAIN_AVM)) { sleep_ms(5000); fprintf(stderr, "Fatal error: invalid main.avm packbeam\n"); + if (avmpack_is_valid(LIB_AVM, (uintptr_t) MAIN_AVM - (uintptr_t) LIB_AVM)) { + fprintf(stderr, "Lib avm packbeam is valid, though\n"); + } AVM_ABORT(); } - if (!avmpack_find_section_by_flag(MAIN_AVM, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { + if (!avmpack_find_section_by_flag(MAIN_AVM, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { sleep_ms(5000); fprintf(stderr, "Fatal error: Failed to locate start module in main.avm packbeam. (Did you flash a library by mistake?)"); AVM_ABORT(); diff --git a/src/platforms/stm32/src/main.c b/src/platforms/stm32/src/main.c index 7febe37717..689aa925f1 100644 --- a/src/platforms/stm32/src/main.c +++ b/src/platforms/stm32/src/main.c @@ -248,7 +248,7 @@ int main() port_driver_init_all(glb); nif_collection_init_all(glb); - if (!avmpack_is_valid(flashed_avm, size) || !avmpack_find_section_by_flag(flashed_avm, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { + if (!avmpack_is_valid(flashed_avm, size) || !avmpack_find_section_by_flag(flashed_avm, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { AVM_LOGE(TAG, "Invalid AVM Pack"); AVM_ABORT(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 42ef857dda..c7652f4b65 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -24,12 +24,14 @@ project (tests) add_executable(test-erlang test.c) add_executable(test-enif test-enif.c) add_executable(test-heap test-heap.c) +add_executable(test-jit_stream_flash test-jit_stream_flash.c ../src/libAtomVM/jit_stream_flash.c) add_executable(test-mailbox test-mailbox.c) add_executable(test-structs test-structs.c) target_compile_features(test-erlang PUBLIC c_std_11) target_compile_features(test-enif PUBLIC c_std_11) target_compile_features(test-heap PUBLIC c_std_11) +target_compile_features(test-jit_stream_flash PUBLIC c_std_11) target_compile_features(test-mailbox PUBLIC c_std_11) target_compile_features(test-structs PUBLIC c_std_11) @@ -37,6 +39,7 @@ if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-erlang PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-enif PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-heap PUBLIC -Wall -pedantic -Wextra -ggdb) + target_compile_options(test-jit_stream_flash PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-mailbox PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-structs PUBLIC -Wall -pedantic -Wextra -ggdb) endif() @@ -50,6 +53,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_link_libraries(test-erlang PRIVATE ${LIBRT}) target_link_libraries(test-enif PRIVATE ${LIBRT}) target_link_libraries(test-heap PRIVATE ${LIBRT}) + target_link_libraries(test-jit_stream_flash PRIVATE ${LIBRT}) target_link_libraries(test-mailbox PRIVATE ${LIBRT}) target_link_libraries(test-structs PRIVATE ${LIBRT}) else() @@ -63,6 +67,7 @@ if (MbedTLS_FOUND) target_link_libraries(test-erlang PRIVATE MbedTLS::mbedtls) target_link_libraries(test-enif PRIVATE MbedTLS::mbedtls) target_link_libraries(test-heap PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-jit_stream_flash PRIVATE MbedTLS::mbedtls) target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) endif() @@ -79,6 +84,7 @@ if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR target_include_directories(test-erlang PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-enif PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-heap PRIVATE ../src/platforms/generic_unix/lib) + target_include_directories(test-jit_stream_flash PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-mailbox PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-structs PRIVATE ../src/platforms/generic_unix/lib) else() @@ -88,11 +94,15 @@ endif() target_include_directories(test-erlang PRIVATE ../src/libAtomVM) target_include_directories(test-enif PRIVATE ../src/libAtomVM) target_include_directories(test-heap PRIVATE ../src/libAtomVM) +target_include_directories(test-jit_stream_flash PRIVATE ../src/libAtomVM ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(test-mailbox PRIVATE ../src/libAtomVM) target_include_directories(test-structs PRIVATE ../src/libAtomVM) target_link_libraries(test-erlang PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-enif PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-heap PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) +# test-jit_stream_flash includes jit_stream_flash.c and provides its own mock platform implementation +target_compile_definitions(test-jit_stream_flash PRIVATE TEST_JIT_STREAM_FLASH) +target_link_libraries(test-jit_stream_flash PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-mailbox PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-structs PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) @@ -120,11 +130,13 @@ if (COVERAGE) append_coverage_compiler_flags_to_target(test-erlang) append_coverage_compiler_flags_to_target(test-enif) append_coverage_compiler_flags_to_target(test-heap) + append_coverage_compiler_flags_to_target(test-jit_stream_flash) append_coverage_compiler_flags_to_target(test-mailbox) append_coverage_compiler_flags_to_target(test-structs) append_coverage_linker_flags_to_target(test-erlang) append_coverage_linker_flags_to_target(test-enif) append_coverage_linker_flags_to_target(test-heap) + append_coverage_linker_flags_to_target(test-jit_stream_flash) append_coverage_linker_flags_to_target(test-mailbox) append_coverage_linker_flags_to_target(test-structs) if (CMAKE_COMPILER_IS_GNUCC) diff --git a/src/platforms/rp2/src/lib/jit_stream_flash.c b/tests/jit_stream_flash_platform.h similarity index 69% rename from src/platforms/rp2/src/lib/jit_stream_flash.c rename to tests/jit_stream_flash_platform.h index 77dfcca908..b9e39dd36b 100644 --- a/src/platforms/rp2/src/lib/jit_stream_flash.c +++ b/tests/jit_stream_flash_platform.h @@ -18,17 +18,23 @@ * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ -#ifndef AVM_NO_JIT +#ifndef _JIT_STREAM_FLASH_PLATFORM_H_ +#define _JIT_STREAM_FLASH_PLATFORM_H_ -#include "context.h" -#include "jit.h" -#include "term.h" +#include +#include +#include -ModuleNativeEntryPoint jit_stream_entry_point(Context *ctx, term jit_stream) -{ - UNUSED(ctx); - UNUSED(jit_stream); - return NULL; -} +#ifdef __cplusplus +extern "C" { +#endif + +// Host test flash constants +#define FLASH_SECTOR_SIZE 4096 +#define FLASH_PAGE_SIZE 256 +#ifdef __cplusplus +} #endif + +#endif // _JIT_STREAM_FLASH_PLATFORM_H_ diff --git a/tests/libs/jit/jit_armv6m_tests.erl b/tests/libs/jit/jit_armv6m_tests.erl index 6bad3cd07b..dee45cc1d9 100644 --- a/tests/libs/jit/jit_armv6m_tests.erl +++ b/tests/libs/jit/jit_armv6m_tests.erl @@ -32,7 +32,7 @@ -define(BACKEND, jit_armv6m). % disassembly obtained with: -% arm-elf-objdump -b binary -D dump.bin -M arm +% arm-elf-objdump -D -b binary -marm --disassembler-options=force-thumb -z call_primitive_0_test() -> State0 = ?BACKEND:new(?JIT_VARIANT_PIC, jit_stream_binary, jit_stream_binary:new(0)), @@ -2057,61 +2057,47 @@ wait_timeout_test() -> wait_test() -> State0 = ?BACKEND:new(?JIT_VARIANT_PIC, jit_stream_binary, jit_stream_binary:new(0)), - State1 = ?BACKEND:jump_table(State0, 5), + State1 = ?BACKEND:jump_table(State0, 2), State2 = ?BACKEND:add_label(State1, 1), Label = 2, State3 = ?BACKEND:set_continuation_to_label(State2, Label), State4 = ?BACKEND:call_primitive_last(State3, ?PRIM_SCHEDULE_WAIT_CP, [ctx, jit_state]), + State5 = ?BACKEND:add_label(State4, 2), + State6 = ?BACKEND:add_label(State5, 0), + State7 = ?BACKEND:update_branches(State6), - Stream = ?BACKEND:stream(State4), + Stream = ?BACKEND:stream(State7), Dump = << " 0: 4b01 ldr r3, [pc, #4] ; (0x8)\n" " 2: b5f2 push {r1, r4, r5, r6, r7, lr}\n" " 4: 449f add pc, r3\n" " 6: 46c0 nop ; (mov r8, r8)\n" - " 8: 0000 movs r0, r0\n" + " 8: 0034 movs r4, r6\n" " a: 0000 movs r0, r0\n" " c: 4b01 ldr r3, [pc, #4] ; (0x14)\n" " e: b5f2 push {r1, r4, r5, r6, r7, lr}\n" " 10: 449f add pc, r3\n" " 12: 46c0 nop ; (mov r8, r8)\n" - " 14: 0000 movs r0, r0\n" + " 14: 0010 movs r0, r2\n" " 16: 0000 movs r0, r0\n" " 18: 4b01 ldr r3, [pc, #4] ; (0x20)\n" " 1a: b5f2 push {r1, r4, r5, r6, r7, lr}\n" " 1c: 449f add pc, r3\n" " 1e: 46c0 nop ; (mov r8, r8)\n" - " 20: 0000 movs r0, r0\n" + " 20: 001c movs r4, r3\n" " 22: 0000 movs r0, r0\n" - " 24: 4b01 ldr r3, [pc, #4] ; (0x2c)\n" - " 26: b5f2 push {r1, r4, r5, r6, r7, lr}\n" - " 28: 449f add pc, r3\n" - " 2a: 46c0 nop ; (mov r8, r8)\n" - " 2c: 0000 movs r0, r0\n" - " 2e: 0000 movs r0, r0\n" - " 30: 4b01 ldr r3, [pc, #4] ; (0x38)\n" - " 32: b5f2 push {r1, r4, r5, r6, r7, lr}\n" - " 34: 449f add pc, r3\n" - " 36: 46c0 nop ; (mov r8, r8)\n" - " 38: 0000 movs r0, r0\n" - " 3a: 0000 movs r0, r0\n" - " 3c: 4b01 ldr r3, [pc, #4] ; (0x44)\n" - " 3e: b5f2 push {r1, r4, r5, r6, r7, lr}\n" - " 40: 449f add pc, r3\n" - " 42: 46c0 nop ; (mov r8, r8)\n" - " 44: 0000 movs r0, r0\n" - " 46: 0000 movs r0, r0\n" - " 48: a700 add r7, pc, #0 ; (adr r7, 0x4c)\n" - " 4a: 2633 movs r6, #51 ; 0x33\n" - " 4c: 4276 negs r6, r6\n" - " 4e: 19f6 adds r6, r6, r7\n" - " 50: 9f00 ldr r7, [sp, #0]\n" - " 52: 607e str r6, [r7, #4]\n" - " 54: 6f57 ldr r7, [r2, #116] ; 0x74\n" - " 56: 9e05 ldr r6, [sp, #20]\n" - " 58: 9705 str r7, [sp, #20]\n" - " 5a: 46b6 mov lr, r6\n" - " 5c: bdf2 pop {r1, r4, r5, r6, r7, pc}" + " 24: a700 add r7, pc, #0 ; (adr r7, 0x28)\n" + " 26: 260f movs r6, #15\n" + " 28: 4276 negs r6, r6\n" + " 2a: 19f6 adds r6, r6, r7\n" + " 2c: 9f00 ldr r7, [sp, #0]\n" + " 2e: 607e str r6, [r7, #4]\n" + " 30: 6f57 ldr r7, [r2, #116] ; 0x74\n" + " 32: 9e05 ldr r6, [sp, #20]\n" + " 34: 9705 str r7, [sp, #20]\n" + " 36: 46b6 mov lr, r6\n" + " 38: bdf2 pop {r1, r4, r5, r6, r7, pc}\n" + " 3a: 46c0 nop ; (mov r8, r8)" >>, ?assertEqual(dump_to_bin(Dump), Stream). diff --git a/tests/test-jit_stream_flash.c b/tests/test-jit_stream_flash.c new file mode 100644 index 0000000000..d35b565584 --- /dev/null +++ b/tests/test-jit_stream_flash.c @@ -0,0 +1,858 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 by Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include +#include +#include + +#include "avmpack.h" +#include "context.h" +#include "globalcontext.h" +#include "jit_stream_flash.h" +#include "jit_stream_flash_platform.h" +#include "scheduler.h" +#include "synclist.h" +#include "term.h" +#include "utils.h" + +// Mock flash memory - simulate 64KB of flash +#define MOCK_FLASH_SIZE (64 * 1024) +// Align to sector boundary for proper flash simulation +static uint8_t mock_flash[MOCK_FLASH_SIZE] __attribute__((aligned(FLASH_SECTOR_SIZE))); + +// JIT entry header (copied from jit_stream_flash.c for testing) +struct JITEntry +{ + uint16_t magic; + uint16_t version; + uint32_t code; + uint32_t labels; + uint32_t size; +}; + +// CRC32 for verification (copied from jit_stream_flash.c) +static uint32_t crc32(const uint8_t *data, size_t len) +{ + uint32_t crc = 0xFFFFFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1)); + } + } + return ~crc; +} + +// Platform context (opaque) +struct JSFlashPlatformContext +{ + uintptr_t base_addr; +}; + +// Forward declarations of mock platform functions +struct JSFlashPlatformContext *jit_stream_flash_platform_init(void); +void jit_stream_flash_platform_destroy(struct JSFlashPlatformContext *ctx); +bool jit_stream_flash_platform_erase_sector(struct JSFlashPlatformContext *ctx, uintptr_t addr); +bool jit_stream_flash_platform_write_page(struct JSFlashPlatformContext *ctx, uintptr_t addr, const uint8_t *data); +uintptr_t jit_stream_flash_platform_ptr_to_executable(uintptr_t addr); + +// Mock platform implementation +struct JSFlashPlatformContext *jit_stream_flash_platform_init(void) +{ + struct JSFlashPlatformContext *ctx = malloc(sizeof(struct JSFlashPlatformContext)); + if (!ctx) { + return NULL; + } + + // DO NOT erase flash here - it should persist across multiple stream creations + // Flash initialization happens once at test startup + + ctx->base_addr = (uintptr_t) mock_flash; + return ctx; +} + +void jit_stream_flash_platform_destroy(struct JSFlashPlatformContext *ctx) +{ + free(ctx); +} + +bool jit_stream_flash_platform_erase_sector(struct JSFlashPlatformContext *ctx, uintptr_t addr) +{ + assert(ctx); + + // Check alignment + if ((addr - ctx->base_addr) % FLASH_SECTOR_SIZE != 0) { + fprintf(stderr, "Erase address 0x%lx not sector-aligned\n", (unsigned long) addr); + return false; + } + + size_t offset = addr - ctx->base_addr; + if (offset >= MOCK_FLASH_SIZE) { + fprintf(stderr, "Erase address 0x%lx out of bounds\n", (unsigned long) addr); + return false; + } + + // Erase the sector + memset(&mock_flash[offset], 0xFF, FLASH_SECTOR_SIZE); + + return true; +} + +bool jit_stream_flash_platform_write_page(struct JSFlashPlatformContext *ctx, uintptr_t addr, const uint8_t *data) +{ + assert(ctx); + + // Check alignment + if ((addr - ctx->base_addr) % FLASH_PAGE_SIZE != 0) { + fprintf(stderr, "Write address 0x%lx not page-aligned (base_addr=0x%lx, offset=0x%lx)\n", + (unsigned long) addr, (unsigned long) ctx->base_addr, + (unsigned long) (addr - ctx->base_addr)); + return false; + } + + size_t offset = addr - ctx->base_addr; + if (offset + FLASH_PAGE_SIZE > MOCK_FLASH_SIZE) { + fprintf(stderr, "Write at offset 0x%zx would exceed flash bounds\n", offset); + return false; + } + + // Validate write - flash can only transition bits from 1→0 without erase + for (size_t i = 0; i < FLASH_PAGE_SIZE; i++) { + uint8_t current = mock_flash[offset + i]; + uint8_t new_val = data[i]; + + // Check if we're trying to set any bits from 0→1 + if ((~current & new_val) != 0) { + fprintf(stderr, "FLASH VALIDATION ERROR at offset 0x%zx:\n", offset + i); + fprintf(stderr, " Attempting to set bits 0→1 without erase\n"); + fprintf(stderr, " Current: 0x%02x, New: 0x%02x, Invalid bits: 0x%02x\n", + current, new_val, ~current & new_val); + return false; + } + } + + // Write the page + memcpy(&mock_flash[offset], data, FLASH_PAGE_SIZE); + + return true; +} + +uintptr_t jit_stream_flash_platform_ptr_to_executable(uintptr_t addr) +{ + // For host testing, no conversion needed + return addr; +} + +uintptr_t jit_stream_flash_platform_executable_to_ptr(uintptr_t addr) +{ + // For host testing, no conversion needed + return addr; +} + +// Create a minimal AVM pack for testing +static uint8_t create_minimal_avmpack(void) +{ + // Create a minimal AVM pack with an "end" section + uint8_t *pack = mock_flash + 0x100; // Place pack at offset 0x100 + + // AVM Pack header: "#!/usr/bin/env AtomVM\n" (23 bytes) + padding to 24 bytes + const char header_str[] = "#!/usr/bin/env AtomVM\n"; + memcpy(pack, header_str, 23); + pack[23] = 0; // Padding to align to 4 bytes + + // Section header for "end" section + uint8_t *section = pack + 24; + uint32_t *sec_header = (uint32_t *) section; + + // Section format: size (4) + flags (4) + reserved (4) + name (null-terminated) + // Write size in big-endian (total section size including header) + uint32_t section_size = 4 + 4 + 4 + 4; // size + flags + reserved + "end\0" + sec_header[0] = __builtin_bswap32(section_size); + + // Write flags in big-endian + uint32_t flags = END_OF_FILE; + sec_header[1] = __builtin_bswap32(flags); + + // Write reserved field (seems to be 0) + sec_header[2] = 0; + + // Write null-terminated name starting at offset 12 + memcpy(section + 12, "end", 4); // includes null terminator + + return 0; +} + +// Register AVM pack with global context +static void register_test_avmpack(GlobalContext *glb) +{ + create_minimal_avmpack(); + + // Create AVMPackData + struct ConstAVMPack *pack = malloc(sizeof(struct ConstAVMPack)); + avmpack_data_init(&pack->base, &const_avm_pack_info); + pack->base.data = mock_flash + 0x100; + pack->base.in_use = true; + + // Add to global context's avmpack list + synclist_append(&glb->avmpack_data, &pack->base.avmpack_head); +} + +// Test helper: create binary term with proper GC rooting +static term make_binary_rooted(Context *ctx, const uint8_t *data, size_t len, term *roots, int num_roots) +{ + if (UNLIKELY(memory_ensure_free_with_roots(ctx, term_binary_heap_size(len), num_roots, roots, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + return term_invalid_term(); + } + return term_from_literal_binary(data, len, &ctx->heap, ctx->global); +} + +// Test helper: get NIF function +typedef term (*nif_function)(Context *ctx, int argc, term argv[]); + +static nif_function get_nif(const char *name) +{ + const struct Nif *nif = jit_stream_flash_get_nif(name); + if (!nif || nif->base.type != NIFFunctionType) { + return NULL; + } + return nif->nif_ptr; +} + +// Test 1: Basic append and flush +void test_basic_append_flush(void) +{ + fprintf(stderr, "\n=== Test: Basic Append and Flush ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + assert(new_nif != NULL); + assert(append_nif != NULL); + assert(flush_nif != NULL); + + // Create stream + term argv[3]; + argv[0] = term_from_int(10); // label count + term stream = new_nif(ctx, 1, argv); + assert(term_is_binary(stream)); // Resource is a binary + + // Append some data - root the stream during binary allocation + uint8_t data[100]; + memset(data, 0xAA, sizeof(data)); + argv[0] = stream; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); // Root argv[0] (stream) + stream = append_nif(ctx, 2, argv); // Update stream in case GC moved it + assert(stream == argv[0]); // Should return the stream + + // Flush + argv[0] = stream; + stream = flush_nif(ctx, 1, argv); // Update stream + assert(stream == argv[0]); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Basic append and flush\n"); +} + +// Test 2: Multiple appends crossing page boundaries +void test_multiple_appends(void) +{ + fprintf(stderr, "\n=== Test: Multiple Appends Crossing Pages ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Create stream + term argv[3]; + argv[0] = term_from_int(10); + term stream = new_nif(ctx, 1, argv); + + // Append multiple chunks to cross page boundaries + for (int i = 0; i < 10; i++) { + uint8_t data[100]; + memset(data, 0xA0 + i, sizeof(data)); + argv[0] = stream; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream = append_nif(ctx, 2, argv); + argv[0] = stream; // Update for next iteration + } + + // Flush + argv[0] = stream; + flush_nif(ctx, 1, argv); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Multiple appends crossing pages\n"); +} + +// Test 3: Replace operation +void test_replace(void) +{ + fprintf(stderr, "\n=== Test: Replace Operation ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function replace_nif = get_nif("jit_stream_flash:replace/3"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Create stream + term argv[3]; + argv[0] = term_from_int(10); + term stream = new_nif(ctx, 1, argv); + + // Append initial data + uint8_t data[200]; + memset(data, 0xAA, sizeof(data)); + argv[0] = stream; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream = append_nif(ctx, 2, argv); // Update stream + + // Replace some bytes in the middle + uint8_t replace_data[] = { 0x11, 0x22, 0x33, 0x44 }; + argv[0] = stream; + argv[1] = term_from_int(50); // offset + argv[2] = make_binary_rooted(ctx, replace_data, sizeof(replace_data), &argv[0], 1); + stream = replace_nif(ctx, 3, argv); // Update stream + + // Flush + argv[0] = stream; + stream = flush_nif(ctx, 1, argv); // Update stream + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Replace operation\n"); +} + +// Test 4: Second module bug scenario - this is the critical test! +void test_second_module_bug(void) +{ + fprintf(stderr, "\n=== Test: Second Module Bug Scenario (THE ACTUAL BUG) ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Simulate first module compilation - fill most of first sector + fprintf(stderr, "Simulating first module compilation...\n"); + term argv[3]; + argv[0] = term_from_int(100); + term stream1 = new_nif(ctx, 1, argv); + + // Write 3.5KB of code (leaves 0.5KB in first sector) + for (int i = 0; i < 35; i++) { + uint8_t data[100]; + memset(data, 0xA0 + (i % 16), sizeof(data)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); // Update stream1 + } + + argv[0] = stream1; + + stream1 = flush_nif(ctx, 1, argv); // Update stream1 + + fprintf(stderr, "First module compiled and flushed\n"); + + // Finalize the first module to mark it as valid and prepare for the second + ModuleNativeEntryPoint entry1 = jit_stream_flash_entry_point(ctx, stream1); + Module fake_mod1; + fake_mod1.code = (CodeChunk *) 0x12345678; // Fake code pointer for testing + + globalcontext_set_cache_native_code(glb, &fake_mod1, 1, entry1, 100); + + // Now simulate second module - this should trigger the bug + // The bug was: when creating a new stream, if we're in a new sector + // that hasn't been erased, we need to erase it before writing + fprintf(stderr, "\nSimulating second module compilation...\n"); + argv[0] = term_from_int(50); + term stream2 = new_nif(ctx, 1, argv); + + // Append data - this will cross into next sector + for (int i = 0; i < 20; i++) { + uint8_t data[100]; + memset(data, 0xB0 + (i % 16), sizeof(data)); + argv[0] = stream2; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream2 = append_nif(ctx, 2, argv); // Update stream2 + } + + argv[0] = stream2; + stream2 = flush_nif(ctx, 1, argv); // Update stream2 + + fprintf(stderr, "Second module compiled and flushed successfully!\n"); + + // Finalize the second module + ModuleNativeEntryPoint entry2 = jit_stream_flash_entry_point(ctx, stream2); + Module fake_mod2; + fake_mod2.code = (CodeChunk *) 0x87654321; // Fake code pointer for testing + globalcontext_set_cache_native_code(glb, &fake_mod2, 1, entry2, 50); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Second module bug scenario - bug is FIXED!\n"); +} + +void test_magic_0xffff_but_garbage_bug(void) +{ + fprintf(stderr, "\n=== Test: Magic is 0xFFFF but Sector Has Garbage ===\n"); + + // Simulate ESP32 scenario where first JIT entry is at start of sector + // and magic happens to be 0xFFFF but rest has garbage + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first sector with AVM + + // Set magic to 0xFFFF at start of sector 1, but rest is garbage (0x97) + uint16_t *magic_ptr = (uint16_t *) (mock_flash + 0x1000); + *magic_ptr = 0xFFFF; + // Fill rest of sector with garbage + for (size_t i = 2; i < FLASH_SECTOR_SIZE; i++) { + mock_flash[0x1000 + i] = 0x97; + } + + fprintf(stderr, "Sector 1: magic=0xFFFF at offset 0, but rest has garbage (0x97)\n"); + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Compile a small module - should detect garbage and erase + fprintf(stderr, "Compiling module (should detect garbage despite magic=0xFFFF)...\n"); + term argv[3]; + argv[0] = term_from_int(100); + term stream1 = new_nif(ctx, 1, argv); + + // Append some data + uint8_t data[100]; + memset(data, 0xAA, sizeof(data)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); + + argv[0] = stream1; + stream1 = flush_nif(ctx, 1, argv); + fprintf(stderr, "Module compiled successfully!\n"); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Magic 0xFFFF but garbage test\n"); +} + +void test_garbage_flash_bug(void) +{ + fprintf(stderr, "\n=== Test: Garbage Flash Bug - JIT Sectors Not Erased After AVM Flash ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + fprintf(stderr, "Flash state: Sector 0 erased (0xFF), sectors 1+ have garbage (0x00)\n"); + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Compile first module that spans two sectors (like benchmark: 8254 bytes) + fprintf(stderr, "Compiling first module spanning sectors 1-2 (8254 bytes)...\n"); + term argv[3]; + argv[0] = term_from_int(100); + term stream1 = new_nif(ctx, 1, argv); + + // Write 82 blocks of 100 bytes = 8200 bytes + 16 byte header = 8216 bytes + for (int i = 0; i < 82; i++) { + uint8_t data[100]; + memset(data, 0xAA, sizeof(data)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); + } + + argv[0] = stream1; + stream1 = flush_nif(ctx, 1, argv); + fprintf(stderr, "First module compiled and flushed\n"); + + ModuleNativeEntryPoint entry1 = jit_stream_flash_entry_point(ctx, stream1); + Module fake_mod1; + fake_mod1.code = (CodeChunk *) 0x12345678; + globalcontext_set_cache_native_code(glb, &fake_mod1, 1, entry1, 100); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Garbage flash bug test\n"); +} + +void test_esp32_crash_bug(void) +{ + fprintf(stderr, "\n=== Test: ESP32 Crash Bug - Module Spanning Multiple Sectors ===\n"); + + // Reset flash for this test + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); // first page with AVM + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Simulate first module like ESP32 benchmark: ~8254 bytes + // This will span sectors 0, 1, and part of sector 2 + fprintf(stderr, "First module: writing ~8254 bytes (spans 3 sectors)...\n"); + term argv[3]; + argv[0] = term_from_int(100); + term stream1 = new_nif(ctx, 1, argv); + + // Write 82 blocks of 100 bytes = 8200 bytes + 16 byte header = 8216 bytes + for (int i = 0; i < 82; i++) { + uint8_t data[100]; + memset(data, 0xAA, sizeof(data)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); + } + + argv[0] = stream1; + stream1 = flush_nif(ctx, 1, argv); + fprintf(stderr, "First module flushed\n"); + + ModuleNativeEntryPoint entry1 = jit_stream_flash_entry_point(ctx, stream1); + Module fake_mod1; + fake_mod1.code = (CodeChunk *) 0x12345678; + globalcontext_set_cache_native_code(glb, &fake_mod1, 1, entry1, 100); + + // Second module like ESP32 pingpong: ~6690 bytes + // This will start in sector 2 (which already has tail of first module!) + fprintf(stderr, "Second module: writing ~6690 bytes...\n"); + argv[0] = term_from_int(50); + term stream2 = new_nif(ctx, 1, argv); + + // Write 67 blocks of 100 bytes = 6700 bytes + for (int i = 0; i < 67; i++) { + uint8_t data[100]; + memset(data, 0xBB, sizeof(data)); + argv[0] = stream2; + argv[1] = make_binary_rooted(ctx, data, sizeof(data), &argv[0], 1); + stream2 = append_nif(ctx, 2, argv); + } + + argv[0] = stream2; + stream2 = flush_nif(ctx, 1, argv); + fprintf(stderr, "Second module flushed\n"); + + ModuleNativeEntryPoint entry2 = jit_stream_flash_entry_point(ctx, stream2); + Module fake_mod2; + fake_mod2.code = (CodeChunk *) 0x87654321; + globalcontext_set_cache_native_code(glb, &fake_mod2, 1, entry2, 50); + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: ESP32 crash bug test\n"); +} + +// Test for the tail corruption bug: when first module extends into next sector, +// creating the second module should NOT erase the sector containing the first module's tail +static void test_tail_corruption_bug(void) +{ + fprintf(stderr, "\n=== Test: Tail Corruption Bug - Module Tail in Next Sector ===\n"); + + // Initialize flash: sector 0 erased (AVM), rest is garbage + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); + + create_minimal_avmpack(); + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Create first module that will extend into the next sector + // Module size: 8270 bytes (like benchmark on ESP32) + // Entry header: 16 bytes at 0x0 in sector 0x1000 + // Native code: 8254 bytes, extends from sector 0x1000 into sector 0x2000 + // Module ends at: 0x1000 + 16 + 8254 = 0x304E (in sector 0x2000) + // Next entry would be at: 0x3050 (also in sector 0x2000) + + term argv[3]; + argv[0] = term_from_int(10); + term stream1 = new_nif(ctx, 1, argv); + + // Append 8254 bytes of native code + uint8_t code1[8254]; + memset(code1, 0xAB, sizeof(code1)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, code1, sizeof(code1), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); + + argv[0] = stream1; + term stream1_flushed = flush_nif(ctx, 1, argv); + ModuleNativeEntryPoint entry1 = jit_stream_flash_entry_point(ctx, stream1_flushed); + + Module fake_mod1; + fake_mod1.code = (CodeChunk *) 0x12345678; + globalcontext_set_cache_native_code(glb, &fake_mod1, 1, entry1, 30); + + // Compute CRC of first module for verification + uintptr_t data_addr1 = jit_stream_flash_platform_executable_to_ptr((uintptr_t) entry1); + struct JITEntry *jit_entry1 = (struct JITEntry *) (data_addr1 - sizeof(struct JITEntry)); + uint32_t crc1_after_finalize = crc32((const uint8_t *) jit_entry1, sizeof(struct JITEntry) + jit_entry1->size); + fprintf(stderr, "First module: entry=%p size=%u CRC=0x%08x\n", + (void *) jit_entry1, (unsigned int) jit_entry1->size, (unsigned int) crc1_after_finalize); + + // Verify first module extends into sector 0x2000 + uintptr_t entry1_addr = (uintptr_t) jit_entry1; + uintptr_t entry1_end = entry1_addr + sizeof(struct JITEntry) + jit_entry1->size; + uintptr_t entry1_sector = entry1_addr & ~(FLASH_SECTOR_SIZE - 1); + uintptr_t entry1_end_sector = entry1_end & ~(FLASH_SECTOR_SIZE - 1); + fprintf(stderr, "First module: starts in sector 0x%lx, ends at 0x%lx (sector 0x%lx)\n", + (unsigned long) entry1_sector, (unsigned long) entry1_end, + (unsigned long) entry1_end_sector); + + if (entry1_sector == entry1_end_sector) { + fprintf(stderr, "FAIL: Test setup error - first module should span sectors\n"); + exit(1); + } + + // Create second module - THIS SHOULD NOT CORRUPT THE FIRST MODULE + argv[0] = term_from_int(10); + term stream2 = new_nif(ctx, 1, argv); + + uint8_t code2[100]; + memset(code2, 0xCD, sizeof(code2)); + argv[0] = stream2; + argv[1] = make_binary_rooted(ctx, code2, sizeof(code2), &argv[0], 1); + stream2 = append_nif(ctx, 2, argv); + + argv[0] = stream2; + term stream2_flushed = flush_nif(ctx, 1, argv); + ModuleNativeEntryPoint entry2 = jit_stream_flash_entry_point(ctx, stream2_flushed); + + Module fake_mod2; + fake_mod2.code = (CodeChunk *) 0x87654321; + globalcontext_set_cache_native_code(glb, &fake_mod2, 1, entry2, 20); + + // Verify first module's CRC is still intact + uint32_t crc1_after_second = crc32((const uint8_t *) jit_entry1, sizeof(struct JITEntry) + jit_entry1->size); + fprintf(stderr, "First module after second: CRC=0x%08x (expected 0x%08x)\n", + (unsigned int) crc1_after_second, (unsigned int) crc1_after_finalize); + + if (crc1_after_second != crc1_after_finalize) { + fprintf(stderr, "FAIL: First module corrupted after creating second module!\n"); + fprintf(stderr, "Expected CRC: 0x%08x, Got: 0x%08x\n", + (unsigned int) crc1_after_finalize, (unsigned int) crc1_after_second); + exit(1); + } + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Tail corruption bug test\n"); +} + +// Test 9: Stale data cleanup after failed compilation +static void test_stale_data_cleanup(void) +{ + fprintf(stderr, "\n=== Test: Stale Data Cleanup After Failed Compilation ===\n"); + + // Initialize flash: sector 0 erased (AVM), rest is garbage + memset(mock_flash, 0x00, MOCK_FLASH_SIZE); + memset(&mock_flash[0], 0xFF, FLASH_SECTOR_SIZE); + + create_minimal_avmpack(); + + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + register_test_avmpack(glb); + jit_stream_flash_init(glb); + + nif_function new_nif = get_nif("jit_stream_flash:new/1"); + nif_function append_nif = get_nif("jit_stream_flash:append/2"); + nif_function flush_nif = get_nif("jit_stream_flash:flush/1"); + + // Create first module and finalize it (small, stays in first sector after AVM) + term argv[3]; + argv[0] = term_from_int(10); + term stream1 = new_nif(ctx, 1, argv); + + uint8_t code1[500]; + memset(code1, 0xAA, sizeof(code1)); + argv[0] = stream1; + argv[1] = make_binary_rooted(ctx, code1, sizeof(code1), &argv[0], 1); + stream1 = append_nif(ctx, 2, argv); + + argv[0] = stream1; + term stream1_flushed = flush_nif(ctx, 1, argv); + ModuleNativeEntryPoint entry1 = jit_stream_flash_entry_point(ctx, stream1_flushed); + + Module fake_mod1; + fake_mod1.code = (CodeChunk *) 0x12345678; + globalcontext_set_cache_native_code(glb, &fake_mod1, 1, entry1, 30); + + // Compute CRC of first module + uintptr_t data_addr1 = jit_stream_flash_platform_executable_to_ptr((uintptr_t) entry1); + struct JITEntry *jit_entry1 = (struct JITEntry *) (data_addr1 - sizeof(struct JITEntry)); + uint32_t crc1_original = crc32((const uint8_t *) jit_entry1, sizeof(struct JITEntry) + jit_entry1->size); + fprintf(stderr, "First module: CRC=0x%08x, size=%u bytes\n", + (unsigned int) crc1_original, (unsigned int) jit_entry1->size); + + // Start creating a second module but DON'T finalize (simulate crash/OOM) + argv[0] = term_from_int(10); + term stream2_attempt1 = new_nif(ctx, 1, argv); + + uint8_t code2[200]; + memset(code2, 0xBB, sizeof(code2)); + argv[0] = stream2_attempt1; + argv[1] = make_binary_rooted(ctx, code2, sizeof(code2), &argv[0], 1); + stream2_attempt1 = append_nif(ctx, 2, argv); + + // DON'T flush or finalize - this simulates a failed compilation + // Now there's stale data in flash after the first module + + fprintf(stderr, "Simulated failed compilation - stale data left in flash\n"); + + // Try to create the second module again - should detect and clean up stale data + argv[0] = term_from_int(10); + term stream2_attempt2 = new_nif(ctx, 1, argv); + + memset(code2, 0xCC, sizeof(code2)); + argv[0] = stream2_attempt2; + argv[1] = make_binary_rooted(ctx, code2, sizeof(code2), &argv[0], 1); + stream2_attempt2 = append_nif(ctx, 2, argv); + + argv[0] = stream2_attempt2; + term stream2_flushed = flush_nif(ctx, 1, argv); + ModuleNativeEntryPoint entry2 = jit_stream_flash_entry_point(ctx, stream2_flushed); + + Module fake_mod2; + fake_mod2.code = (CodeChunk *) 0x87654321; + globalcontext_set_cache_native_code(glb, &fake_mod2, 1, entry2, 20); + + fprintf(stderr, "Second module successfully created after cleanup\n"); + + // Verify first module's CRC is still intact + uint32_t crc1_after_cleanup = crc32((const uint8_t *) jit_entry1, sizeof(struct JITEntry) + jit_entry1->size); + fprintf(stderr, "First module after cleanup: CRC=0x%08x (expected 0x%08x)\n", + (unsigned int) crc1_after_cleanup, (unsigned int) crc1_original); + + if (crc1_after_cleanup != crc1_original) { + fprintf(stderr, "FAIL: First module corrupted during stale data cleanup!\n"); + exit(1); + } + + scheduler_terminate(ctx); + globalcontext_destroy(glb); + + fprintf(stderr, "PASS: Stale data cleanup test\n"); +} + +int main(int argc, char **argv) +{ + UNUSED(argc); + UNUSED(argv); + + fprintf(stderr, "Starting jit_stream_flash tests...\n"); + + test_basic_append_flush(); + test_multiple_appends(); + test_replace(); + test_second_module_bug(); + test_magic_0xffff_but_garbage_bug(); + test_garbage_flash_bug(); + test_esp32_crash_bug(); + test_tail_corruption_bug(); + test_stale_data_cleanup(); + + fprintf(stderr, "\nAll tests passed!\n"); + return EXIT_SUCCESS; +}