@mikrojs/native 0.17.0 → 0.17.1-pr-300.20260731003316

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CMakeLists.txt CHANGED
@@ -86,6 +86,7 @@ mikrojs_generate_bytecode(
86
86
  MODULE_PREFIX "mikro"
87
87
  SYMBOL_PREFIX "mikro"
88
88
  TARGET gen_bytecode
89
+ FREEZE_ATOMS
89
90
  )
90
91
  add_dependencies(mikrojs gen_bytecode)
91
92
  target_include_directories(mikrojs PRIVATE "${gen_bytecode_INCLUDE_DIR}")
@@ -25,7 +25,11 @@ get_filename_component(_MIK_BC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
25
25
  set(_MIK_BC_SCRIPTS_DIR "${_MIK_BC_CMAKE_DIR}/../scripts" CACHE INTERNAL "")
26
26
 
27
27
  function(mikrojs_generate_bytecode)
28
- cmake_parse_arguments(ARG "" "RUNTIME_DIR;MODULE_PREFIX;SYMBOL_PREFIX;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
28
+ # FREEZE_ATOMS: compile blobs against a generated frozen atom table so
29
+ # they load zero-copy (JS_READ_OBJ_INPLACE). Core runtime only: the
30
+ # device preloads exactly one table, so board/driver packages must
31
+ # stay classic (they load via the copy-and-relocate fallback).
32
+ cmake_parse_arguments(ARG "FREEZE_ATOMS" "RUNTIME_DIR;MODULE_PREFIX;SYMBOL_PREFIX;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
29
33
 
30
34
  # Defaults
31
35
  if(NOT ARG_TARGET)
@@ -86,7 +90,46 @@ function(mikrojs_generate_bytecode)
86
90
  WORKING_DIRECTORY ${ARG_WORKING_DIRECTORY}
87
91
  )
88
92
 
89
- # Step 2: Compile each bundled JS file to a bytecode header with qjsc
93
+ # Steps 2a/2b (FREEZE_ATOMS only): pass-1 raw bytecode per module, then
94
+ # union the atom sections into the frozen atom table (frozen_atoms.bin
95
+ # for qjsc -A, <prefix>_frozen_atoms.h for the runtime preload).
96
+ # Module order fixes the table order.
97
+ set(_ATOMS_BIN "")
98
+ set(_ATOMS_HEADER "")
99
+ if(ARG_FREEZE_ATOMS)
100
+ set(_EXTRACT_SCRIPT "${_MIK_BC_SCRIPTS_DIR}/extract-atoms.js")
101
+ set(_PASS1_BLOBS "")
102
+ foreach(mod ${ARG_MODULES})
103
+ string(REGEX REPLACE "[^a-zA-Z0-9]" "_" _mod_safe "${mod}")
104
+ set(_blob "${_GEN_DIR}/pass1/${_mod_safe}.bjs")
105
+ add_custom_command(
106
+ OUTPUT ${_blob}
107
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${_GEN_DIR}/pass1
108
+ COMMAND sh ${_COMPILE_SCRIPT}
109
+ ${QJSC_EXECUTABLE}
110
+ ${_BUNDLE_DIR}/${mod}.js
111
+ ${_blob}
112
+ "${ARG_MODULE_PREFIX}/${mod}"
113
+ "unused"
114
+ DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
115
+ COMMENT "Bytecode pass 1: ${ARG_MODULE_PREFIX}/${mod}"
116
+ )
117
+ list(APPEND _PASS1_BLOBS ${_blob})
118
+ endforeach()
119
+
120
+ set(_ATOMS_BIN "${_GEN_DIR}/frozen_atoms.bin")
121
+ set(_ATOMS_HEADER "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_frozen_atoms.h")
122
+ add_custom_command(
123
+ OUTPUT ${_ATOMS_BIN} ${_ATOMS_HEADER}
124
+ COMMAND node ${_EXTRACT_SCRIPT} ${_GEN_DIR} ${ARG_SYMBOL_PREFIX} ${_PASS1_BLOBS}
125
+ DEPENDS ${_PASS1_BLOBS} ${_EXTRACT_SCRIPT}
126
+ COMMENT "Extracting frozen atom table"
127
+ )
128
+ endif()
129
+
130
+ # Step 2c: Compile each bundled JS file to a bytecode header with qjsc,
131
+ # against the frozen atom table so atom references are final ids
132
+ # (enables zero-copy loading via JS_READ_OBJ_INPLACE).
90
133
  set(_HEADERS "")
91
134
  foreach(mod ${ARG_MODULES})
92
135
  # Sanitize module name for C identifiers (replace non-alphanumeric with _)
@@ -101,11 +144,15 @@ function(mikrojs_generate_bytecode)
101
144
  ${_header}
102
145
  "${ARG_MODULE_PREFIX}/${mod}"
103
146
  "${ARG_SYMBOL_PREFIX}_${_mod_safe}_bytecode"
104
- DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
147
+ ${_ATOMS_BIN}
148
+ DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE} ${_ATOMS_BIN}
105
149
  COMMENT "Compiling bytecode: ${ARG_MODULE_PREFIX}/${mod}"
106
150
  )
107
151
  list(APPEND _HEADERS ${_header})
108
152
  endforeach()
153
+ if(_ATOMS_HEADER)
154
+ list(APPEND _HEADERS ${_ATOMS_HEADER})
155
+ endif()
109
156
 
110
157
  # Step 3: Generate builtins table header (includes + lookup table)
111
158
  set(_TABLE_HEADER "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_builtins_table.h")
@@ -36,6 +36,7 @@ typedef struct DynBuf {
36
36
 
37
37
  /* Wrapper functions defined in cutils_compat.c */
38
38
  int mik__dbuf_put(DynBuf *s, const void *data, size_t len);
39
+ int mik__dbuf_claim(DynBuf *s, size_t len);
39
40
  int mik__dbuf_putc(DynBuf *s, uint8_t val);
40
41
  int mik__dbuf_putstr(DynBuf *s, const char *str);
41
42
  void mik__dbuf_free(DynBuf *s);
@@ -46,6 +47,7 @@ char *mik__js_pstrcat(char *buf, int buf_size, const char *s);
46
47
 
47
48
  /* Map original names to wrapper names */
48
49
  #define dbuf_put mik__dbuf_put
50
+ #define dbuf_claim mik__dbuf_claim
49
51
  #define dbuf_putc mik__dbuf_putc
50
52
  #define dbuf_putstr mik__dbuf_putstr
51
53
  #define dbuf_free mik__dbuf_free
@@ -288,8 +288,12 @@ extern mik_module_desc_t* mik__module_registry_head;
288
288
  * Driver packages use MIK_REGISTER_BUILTIN() to embed their JS wrappers as
289
289
  * firmware builtins, resolved by the real npm package name (e.g. "@mikrojs/your-driver"). */
290
290
  typedef struct mik_ext_builtin_t {
291
- const char* name; /* module name, e.g. "@mikrojs/your-driver" */
292
- const uint8_t* data; /* compiled bytecode */
291
+ const char* name; /* module name, e.g. "@mikrojs/your-driver" */
292
+ /* Compiled bytecode. Must outlive every runtime that imports the
293
+ * module: the loader may keep instruction streams pointing into this
294
+ * buffer instead of copying them to the heap (JS_READ_OBJ_INPLACE).
295
+ * Use a static const array (flash/rodata), never a heap buffer. */
296
+ const uint8_t* data;
293
297
  uint32_t data_size;
294
298
  struct mik_ext_builtin_t* next;
295
299
  } mik_ext_builtin_t;
@@ -187,6 +187,9 @@ void mik__flush_unhandled_rejections(JSContext* ctx);
187
187
  /* Drop a promise from the pending-rejection queue without reporting it. */
188
188
  void mik__forget_rejection(JSContext* ctx, JSValue promise);
189
189
  JSModuleDef* mik__load_builtin(JSContext* ctx, const char* name);
190
+ /* Preload the generated frozen atom table (must run right after
191
+ * JS_NewRuntime2, before any context/intrinsic creates atoms). */
192
+ int mik__preload_frozen_atoms(JSRuntime* rt);
190
193
  int mik__load_file(JSContext* ctx, DynBuf* dbuf, const char* filename);
191
194
  void mik__resolve_fs_path(JSContext* ctx, const char* module_name, char* out, size_t out_size);
192
195
  int mik__resolve_fs_root(JSContext* ctx, const char* path, char* out, size_t out_size);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.17.0",
3
+ "version": "0.17.1-pr-300.20260731003316",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -86,14 +86,14 @@
86
86
  "cmake-js": "^8.0.0",
87
87
  "node-addon-api": "^8.7.0",
88
88
  "node-gyp-build": "^4.8.4",
89
- "@mikrojs/quickjs": "0.17.0"
89
+ "@mikrojs/quickjs": "0.17.1-pr-300.20260731003316+67c29b9"
90
90
  },
91
91
  "devDependencies": {
92
92
  "@swc/core": "^1.15.30",
93
93
  "@types/node": "^24.12.2",
94
94
  "esbuild": "^0.28.0",
95
95
  "terser": "^5.46.2",
96
- "@mikrojs/registry": "0.17.0"
96
+ "@mikrojs/registry": "0.17.1-pr-300.20260731003316+67c29b9"
97
97
  },
98
98
  "engines": {
99
99
  "node": ">=24.0.0"
@@ -1,9 +1,12 @@
1
1
  #!/bin/sh
2
2
  # Compile a bundled JS module to a C bytecode header using qjsc.
3
3
  #
4
- # Usage: compile-bytecode.sh <qjsc> <input.js> <output.h> <module_name> <symbol_name>
4
+ # Usage: compile-bytecode.sh <qjsc> <input.js> <output.h> <module_name> <symbol_name> [atoms.bin]
5
5
  #
6
6
  # Reads <input>.externals for external module declarations.
7
+ # If <output> ends in .bjs, emits raw bytecode instead of a C header
8
+ # (pass 1 of the frozen-atom pipeline; see extract-atoms.js).
9
+ # If [atoms.bin] is given, compiles against that frozen atom table (-A).
7
10
 
8
11
  set -e
9
12
 
@@ -12,6 +15,7 @@ INPUT="$2"
12
15
  OUTPUT="$3"
13
16
  MODULE_NAME="$4"
14
17
  SYMBOL_NAME="$5"
18
+ ATOMS_BIN="$6"
15
19
 
16
20
  EXTERNALS_FILE="${INPUT%.js}.externals"
17
21
 
@@ -34,5 +38,20 @@ fi
34
38
  # so any runtime module that reads `import.meta.*` (e.g. mikrojs/env
35
39
  # accessing import.meta.env) breaks at runtime. The ~1 KB heap saving
36
40
  # from stripping debug isn't worth crippling a spec-standard module API.
37
- # shellcheck disable=SC2086
38
- "$QJSC" -m -s -N "$SYMBOL_NAME" -n "$MODULE_NAME" $M_FLAGS -o "$OUTPUT" "$INPUT"
41
+ A_FLAGS=""
42
+ if [ -n "$ATOMS_BIN" ]; then
43
+ A_FLAGS="-A $ATOMS_BIN"
44
+ fi
45
+
46
+ case "$OUTPUT" in
47
+ *.bjs)
48
+ # Raw bytecode (frozen-atom pass 1). Must use the same flags as the
49
+ # header pass so the atom set matches, minus -N (no C symbol).
50
+ # shellcheck disable=SC2086
51
+ "$QJSC" -b -m -s -n "$MODULE_NAME" $M_FLAGS $A_FLAGS -o "$OUTPUT" "$INPUT"
52
+ ;;
53
+ *)
54
+ # shellcheck disable=SC2086
55
+ "$QJSC" -m -s -N "$SYMBOL_NAME" -n "$MODULE_NAME" $M_FLAGS $A_FLAGS -o "$OUTPUT" "$INPUT"
56
+ ;;
57
+ esac
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ /**
4
+ * Build the frozen atom table from pass-1 raw bytecode blobs.
5
+ *
6
+ * Reads the atom section of each .bjs blob (compiled without a frozen
7
+ * table), unions the narrow plain-string atoms in first-seen order, and
8
+ * emits:
9
+ * - frozen_atoms.bin — binary table for `qjsc -A` (u32le count, then
10
+ * u32le len + bytes per atom)
11
+ * - <symbol_prefix>_frozen_atoms.h — C arrays for JS_PreloadFrozenAtoms
12
+ *
13
+ * Wide-char and symbol atoms are skipped: they stay dynamic, and any
14
+ * function referencing them falls back to the copy-and-relocate load path.
15
+ *
16
+ * Usage: extract-atoms.js <gen_dir> <symbol_prefix> <blob1.bjs> [blob2.bjs ...]
17
+ */
18
+ import {readFileSync, writeFileSync} from 'node:fs'
19
+ import {join} from 'node:path'
20
+
21
+ const JS_ATOM_TYPE_STRING = 1
22
+
23
+ function readLeb128(buf, pos) {
24
+ let v = 0
25
+ let shift = 0
26
+ for (;;) {
27
+ const b = buf[pos++]
28
+ v |= (b & 0x7f) << shift
29
+ if ((b & 0x80) === 0) break
30
+ shift += 7
31
+ }
32
+ return [v >>> 0, pos]
33
+ }
34
+
35
+ const BC_VERSION = 26
36
+ const BC_VERSION_FROZEN = BC_VERSION | 0x40
37
+
38
+ /** Parse the atom section of a classic bytecode blob, returning the
39
+ * narrow plain-string atom names as Buffers. */
40
+ function extractAtoms(blob, path) {
41
+ // Layout: u8 version, u32 checksum, leb128 atom count, entries.
42
+ // A misparse here degrades silently (empty table → classic blobs →
43
+ // green build with zero memory win), so reject anything unexpected.
44
+ if (blob[0] === BC_VERSION_FROZEN) {
45
+ throw new Error(`${path}: pass-1 blob is already frozen-format — pipeline ordering bug`)
46
+ }
47
+ if (blob[0] !== BC_VERSION) {
48
+ throw new Error(`${path}: unknown bytecode version ${blob[0]} (expected ${BC_VERSION})`)
49
+ }
50
+ const [count, afterCount] = readLeb128(blob, 5)
51
+ let pos = afterCount
52
+ const atoms = []
53
+ for (let i = 0; i < count; i++) {
54
+ const type = blob[pos++]
55
+ if (type === 0) {
56
+ pos += 4 // predefined atom id
57
+ continue
58
+ }
59
+ let lenWide
60
+ ;[lenWide, pos] = readLeb128(blob, pos)
61
+ const isWide = lenWide & 1
62
+ const len = lenWide >>> 1
63
+ const size = isWide ? len * 2 : len
64
+ if (type === JS_ATOM_TYPE_STRING && !isWide) {
65
+ atoms.push(blob.subarray(pos, pos + len))
66
+ }
67
+ pos += size
68
+ }
69
+ return atoms
70
+ }
71
+
72
+ function cEscape(buf) {
73
+ let out = ''
74
+ for (const b of buf) {
75
+ if (b >= 0x20 && b < 0x7f && b !== 0x22 && b !== 0x5c && b !== 0x3f) {
76
+ out += String.fromCharCode(b)
77
+ } else {
78
+ // Octal escapes are always 3 digits here, so a following digit
79
+ // cannot extend the escape (unlike \x).
80
+ out += '\\' + b.toString(8).padStart(3, '0')
81
+ }
82
+ }
83
+ return out
84
+ }
85
+
86
+ const [genDir, symbolPrefix, ...blobPaths] = process.argv.slice(2)
87
+ if (!genDir || !symbolPrefix || blobPaths.length === 0) {
88
+ console.error('usage: extract-atoms.js <gen_dir> <symbol_prefix> <blob.bjs>...')
89
+ process.exit(1)
90
+ }
91
+
92
+ const seen = new Set()
93
+ const names = []
94
+ let totalBlobBytes = 0
95
+ for (const path of blobPaths) {
96
+ const blob = readFileSync(path)
97
+ totalBlobBytes += blob.length
98
+ for (const atom of extractAtoms(blob, path)) {
99
+ const key = atom.toString('latin1')
100
+ if (!seen.has(key)) {
101
+ seen.add(key)
102
+ names.push(atom)
103
+ }
104
+ }
105
+ }
106
+
107
+ // Real builtin bundles always introduce atoms; an empty table from
108
+ // non-trivial input means the parse silently missed the atom section.
109
+ if (names.length === 0 && totalBlobBytes > 1024) {
110
+ console.error(
111
+ `extract-atoms: parsed ${blobPaths.length} blobs (${totalBlobBytes} bytes) but found zero atoms — refusing to emit an empty frozen table`,
112
+ )
113
+ process.exit(1)
114
+ }
115
+
116
+ // Binary table for qjsc -A
117
+ const chunks = [Buffer.alloc(4)]
118
+ chunks[0].writeUInt32LE(names.length, 0)
119
+ for (const name of names) {
120
+ const lenBuf = Buffer.alloc(4)
121
+ lenBuf.writeUInt32LE(name.length, 0)
122
+ chunks.push(lenBuf, name)
123
+ }
124
+ writeFileSync(join(genDir, 'frozen_atoms.bin'), Buffer.concat(chunks))
125
+
126
+ // C header for JS_PreloadFrozenAtoms
127
+ let h = '/* Auto-generated by extract-atoms.js — do not edit */\n'
128
+ if (names.length === 0) {
129
+ h += `static const char* const ${symbolPrefix}_frozen_atom_names[] = {NULL};\n`
130
+ h += `static const uint32_t ${symbolPrefix}_frozen_atom_lens[] = {0};\n`
131
+ } else {
132
+ h += `static const char* const ${symbolPrefix}_frozen_atom_names[] = {\n`
133
+ for (const name of names) {
134
+ h += ` "${cEscape(name)}",\n`
135
+ }
136
+ h += '};\n'
137
+ h += `static const uint32_t ${symbolPrefix}_frozen_atom_lens[] = {\n `
138
+ h += names.map((n) => n.length).join(', ')
139
+ h += ',\n};\n'
140
+ }
141
+ h += `enum { ${symbolPrefix}_frozen_atom_count = ${names.length} };\n`
142
+ writeFileSync(join(genDir, `${symbolPrefix}_frozen_atoms.h`), h)
143
+
144
+ console.log(`extract-atoms: ${names.length} frozen atoms from ${blobPaths.length} blobs`)
package/src/builtins.cpp CHANGED
@@ -17,6 +17,19 @@ typedef struct {
17
17
  #include "gen/mikro_builtins_table.h"
18
18
  #define builtins mikro_builtins
19
19
 
20
+ /* Frozen atom table generated alongside the builtin bytecode: preloading it
21
+ * right after runtime creation gives builtin blobs final atom ids, letting
22
+ * their instruction streams load zero-copy from flash/rodata. */
23
+ #include "gen/mikro_frozen_atoms.h"
24
+
25
+ int mik__preload_frozen_atoms(JSRuntime* rt) {
26
+ if (mikro_frozen_atom_count == 0) {
27
+ return 0;
28
+ }
29
+ return JS_PreloadFrozenAtoms(rt, mikro_frozen_atom_names, mikro_frozen_atom_lens,
30
+ mikro_frozen_atom_count);
31
+ }
32
+
20
33
  /* External builtins registered by board/driver packages via MIK_REGISTER_BUILTIN() */
21
34
  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
22
35
  mik_ext_builtin_t* mik__ext_builtin_head = nullptr;
@@ -29,7 +42,10 @@ static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
29
42
  platform->log(MIK_LOG_DEBUG, "mikrojs", "Loading builtin '%s' (%u bytes, bc_version=%u, data_ptr=%p)", name,
30
43
  data_size, data_size > 0 ? data[0] : 0, (void*)data);
31
44
 
32
- JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE);
45
+ /* Builtin bytecode lives in flash/rodata for the runtime's lifetime, so
46
+ * qualifying instruction streams stay in place instead of being copied
47
+ * to the JS heap (falls back per function for non-final atom refs). */
48
+ JSValue obj = JS_ReadObject(ctx, data, data_size, JS_READ_OBJ_BYTECODE | JS_READ_OBJ_INPLACE);
33
49
 
34
50
  if (JS_IsException(obj)) {
35
51
  /* Peek the exception for the log, then re-throw it so the caller
@@ -9,6 +9,10 @@ int mik__dbuf_put(DynBuf *s, const void *data, size_t len) {
9
9
  return dbuf_put(s, data, len);
10
10
  }
11
11
 
12
+ int mik__dbuf_claim(DynBuf *s, size_t len) {
13
+ return dbuf_claim(s, len);
14
+ }
15
+
12
16
  int mik__dbuf_putc(DynBuf *s, uint8_t val) {
13
17
  return dbuf_putc(s, val);
14
18
  }
package/src/mikrojs.cpp CHANGED
@@ -286,6 +286,12 @@ MIKRuntime* MIK_NewRuntimeInternal(MIKRunOptions* options) {
286
286
  CHECK_NOT_NULL(rt);
287
287
  mik_rt->rt = rt;
288
288
 
289
+ /* Builtin blobs are compiled against this table; a mismatch would make
290
+ * every builtin import fail, and failure here can only mean atoms were
291
+ * created before this point (a programming error). Must run before any
292
+ * context/intrinsic setup creates atoms. */
293
+ CHECK(mik__preload_frozen_atoms(rt) == 0);
294
+
289
295
  /* Seed Math.random() with real entropy.
290
296
  * On ESP32 the system clock starts near epoch 0 on every boot, producing
291
297
  * the same seed. Use platform RNG to inject entropy. */
@@ -850,6 +856,17 @@ int mik__load_file(JSContext* ctx, DynBuf* dbuf, const char* filename) {
850
856
  return -1;
851
857
  }
852
858
 
859
+ /* Reserve the exact file size up front: DynBuf grows 1.5x from zero, so
860
+ * letting it grow incrementally peaks at ~2.5x the file size mid-copy.
861
+ * +4 covers the NUL/wrapper suffix callers append after loading. */
862
+ struct stat st;
863
+ if (fstat(fd, &st) == 0 && st.st_size > 0) {
864
+ if (dbuf_claim(dbuf, (size_t)st.st_size + 4)) {
865
+ close(fd);
866
+ return -1;
867
+ }
868
+ }
869
+
853
870
  uint8_t buf[512];
854
871
  ssize_t n;
855
872
  while ((n = read(fd, buf, sizeof(buf))) > 0) {