@mikrojs/native 0.21.0 → 0.21.1-next.20260926222438

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
@@ -95,14 +95,15 @@ endif()
95
95
  # `native:mikro/*` import with an explicit "not available" error (or is
96
96
  # satisfied by a sim stub) instead of a silent "Builtin not available".
97
97
  execute_process(
98
- COMMAND node "${CMAKE_CURRENT_SOURCE_DIR}/scripts/modules-list.js" bytecode --host
99
- OUTPUT_VARIABLE _MIK_HOST_MODULES
98
+ COMMAND node "${CMAKE_CURRENT_SOURCE_DIR}/scripts/modules-list.js" --host
99
+ OUTPUT_VARIABLE _MIK_HOST_LISTS
100
100
  OUTPUT_STRIP_TRAILING_WHITESPACE
101
101
  RESULT_VARIABLE _MIK_HOST_MODULES_RESULT
102
102
  )
103
103
  if(NOT _MIK_HOST_MODULES_RESULT EQUAL 0)
104
- message(FATAL_ERROR "modules-list.js bytecode --host failed")
104
+ message(FATAL_ERROR "modules-list.js --host failed")
105
105
  endif()
106
+ string(JSON _MIK_HOST_MODULES GET "${_MIK_HOST_LISTS}" bytecode)
106
107
  # The list is read at configure time, so configure must re-run when the table
107
108
  # or the script changes.
108
109
  set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
@@ -112,8 +113,6 @@ include(cmake/mikrojs_bytecode.cmake)
112
113
  mikrojs_generate_bytecode(
113
114
  RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
114
115
  MODULES ${_MIK_HOST_MODULES}
115
- MODULE_PREFIX "mikro"
116
- SYMBOL_PREFIX "mikro"
117
116
  TARGET gen_bytecode
118
117
  )
119
118
  add_dependencies(mikrojs gen_bytecode)
@@ -205,7 +204,9 @@ if(BUILD_TESTING)
205
204
  test/runtime_test.cpp
206
205
  test/modules_test.cpp
207
206
  test/virtual_modules_test.cpp
208
- test/ext_builtin_test.cpp
207
+ test/public_module_test.cpp
208
+ test/package_name_test.cpp
209
+ test/app_module_name_test.cpp
209
210
  test/text_encoding_test.cpp
210
211
  test/inspect_test.cpp
211
212
  test/platform_posix_test.cpp
@@ -1,14 +1,12 @@
1
- # mikrojs_generate_bytecode() — Reusable CMake function for compiling TypeScript
2
- # runtime modules to QuickJS bytecode headers.
1
+ # mikrojs_generate_bytecode() — CMake function that compiles the core runtime's
2
+ # TypeScript modules to QuickJS bytecode headers, as the mikro/* builtins.
3
3
  #
4
- # Used by the core runtime, ESP-IDF component, and board/driver packages.
4
+ # Used by the standalone library and the ESP-IDF component.
5
5
  #
6
6
  # Usage:
7
7
  # mikrojs_generate_bytecode(
8
8
  # RUNTIME_DIR <path> # Directory containing <mod>/<mod>.ts files
9
- # MODULES <mod1> <mod2> # Module names to compile
10
- # MODULE_PREFIX <prefix> # Module name prefix (e.g. "mikro" -> "mikro/gpio")
11
- # SYMBOL_PREFIX <prefix> # C symbol prefix (e.g. "mikrojs" -> "mikrojs_pin_bytecode")
9
+ # MODULES <mod1> <mod2> # Module names to compile ("fs" -> "mikro/fs")
12
10
  # TARGET <name> # Custom target name (default: gen_bytecode)
13
11
  # WORKING_DIRECTORY <dir> # Working dir for esbuild (default: CMAKE_CURRENT_SOURCE_DIR)
14
12
  # )
@@ -25,7 +23,7 @@ get_filename_component(_MIK_BC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
25
23
  set(_MIK_BC_SCRIPTS_DIR "${_MIK_BC_CMAKE_DIR}/../scripts" CACHE INTERNAL "")
26
24
 
27
25
  function(mikrojs_generate_bytecode)
28
- cmake_parse_arguments(ARG "" "RUNTIME_DIR;MODULE_PREFIX;SYMBOL_PREFIX;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
26
+ cmake_parse_arguments(ARG "" "RUNTIME_DIR;TARGET;WORKING_DIRECTORY" "MODULES" ${ARGN})
29
27
 
30
28
  # Defaults
31
29
  if(NOT ARG_TARGET)
@@ -42,12 +40,6 @@ function(mikrojs_generate_bytecode)
42
40
  if(NOT ARG_MODULES)
43
41
  message(FATAL_ERROR "mikrojs_generate_bytecode: MODULES is required")
44
42
  endif()
45
- if(NOT ARG_MODULE_PREFIX)
46
- message(FATAL_ERROR "mikrojs_generate_bytecode: MODULE_PREFIX is required")
47
- endif()
48
- if(NOT ARG_SYMBOL_PREFIX)
49
- message(FATAL_ERROR "mikrojs_generate_bytecode: SYMBOL_PREFIX is required")
50
- endif()
51
43
 
52
44
  # Verify qjsc exists (set by quickjs.cmake)
53
45
  if(NOT EXISTS "${QJSC_EXECUTABLE}")
@@ -91,7 +83,7 @@ function(mikrojs_generate_bytecode)
91
83
  foreach(mod ${ARG_MODULES})
92
84
  # Sanitize module name for C identifiers (replace non-alphanumeric with _)
93
85
  string(REGEX REPLACE "[^a-zA-Z0-9]" "_" _mod_safe "${mod}")
94
- set(_header "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_${_mod_safe}.h")
86
+ set(_header "${_GEN_DIR}/mikro_${_mod_safe}.h")
95
87
  add_custom_command(
96
88
  OUTPUT ${_header}
97
89
  COMMAND ${CMAKE_COMMAND} -E make_directory ${_GEN_DIR}
@@ -99,25 +91,25 @@ function(mikrojs_generate_bytecode)
99
91
  ${QJSC_EXECUTABLE}
100
92
  ${_BUNDLE_DIR}/${mod}.js
101
93
  ${_header}
102
- "${ARG_MODULE_PREFIX}/${mod}"
103
- "${ARG_SYMBOL_PREFIX}_${_mod_safe}_bytecode"
94
+ "mikro/${mod}"
95
+ "mikro_${_mod_safe}_bytecode"
104
96
  DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
105
- COMMENT "Compiling bytecode: ${ARG_MODULE_PREFIX}/${mod}"
97
+ COMMENT "Compiling bytecode: mikro/${mod}"
106
98
  )
107
99
  list(APPEND _HEADERS ${_header})
108
100
  endforeach()
109
101
 
110
102
  # Step 3: Generate builtins table header (includes + lookup table)
111
- set(_TABLE_HEADER "${_GEN_DIR}/${ARG_SYMBOL_PREFIX}_builtins_table.h")
103
+ set(_TABLE_HEADER "${_GEN_DIR}/mikro_builtins_table.h")
112
104
  set(_TABLE_CONTENT "/* Auto-generated by mikrojs_generate_bytecode() — do not edit */\n")
113
105
  foreach(mod ${ARG_MODULES})
114
106
  string(REGEX REPLACE "[^a-zA-Z0-9]" "_" _mod_safe "${mod}")
115
- string(APPEND _TABLE_CONTENT "#include \"${ARG_SYMBOL_PREFIX}_${_mod_safe}.h\"\n")
107
+ string(APPEND _TABLE_CONTENT "#include \"mikro_${_mod_safe}.h\"\n")
116
108
  endforeach()
117
- string(APPEND _TABLE_CONTENT "\nstatic const mik_builtin_t ${ARG_SYMBOL_PREFIX}_builtins[] = {\n")
109
+ string(APPEND _TABLE_CONTENT "\nstatic const mik_builtin_t mikro_builtins[] = {\n")
118
110
  foreach(mod ${ARG_MODULES})
119
111
  string(REGEX REPLACE "[^a-zA-Z0-9]" "_" _mod_safe "${mod}")
120
- string(APPEND _TABLE_CONTENT " {\"${ARG_MODULE_PREFIX}/${mod}\", ${ARG_SYMBOL_PREFIX}_${_mod_safe}_bytecode, ${ARG_SYMBOL_PREFIX}_${_mod_safe}_bytecode_size},\n")
112
+ string(APPEND _TABLE_CONTENT " {\"mikro/${mod}\", mikro_${_mod_safe}_bytecode, mikro_${_mod_safe}_bytecode_size},\n")
121
113
  endforeach()
122
114
  string(APPEND _TABLE_CONTENT " {NULL, NULL, 0},\n};\n")
123
115
  file(GENERATE OUTPUT ${_TABLE_HEADER} CONTENT "${_TABLE_CONTENT}")
@@ -138,9 +130,3 @@ function(mikrojs_force_include_modules)
138
130
  target_link_libraries(${COMPONENT_LIB} INTERFACE "-u mik__mod_desc_${id}")
139
131
  endforeach()
140
132
  endfunction()
141
-
142
- function(mikrojs_force_include_builtins)
143
- foreach(id ${ARGN})
144
- target_link_libraries(${COMPONENT_LIB} INTERFACE "-u mik__builtin_${id}")
145
- endforeach()
146
- endfunction()
package/cmake.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** Path to the main CMakeLists.txt */
2
+ export declare const cmakePath: string
3
+ /** Path to the public include directory */
4
+ export declare const includePath: string
5
+ /** Path to the C++ source directory */
6
+ export declare const srcPath: string
7
+ /** Path to the build scripts directory */
8
+ export declare const scriptsPath: string
9
+ /** Path to the runtime TypeScript modules directory */
10
+ export declare const runtimePath: string
11
+ /** Path to the reusable bytecode generation CMake module */
12
+ export declare const bytecodeCmakePath: string
@@ -287,24 +287,45 @@ typedef struct mik_module_desc_t {
287
287
  extern mik_module_desc_t* mik__module_registry_head;
288
288
 
289
289
  /* NOLINTBEGIN(bugprone-macro-parentheses,cppcoreguidelines-avoid-non-const-global-variables) */
290
- /* Namespace governance. The build defines MIK_PACKAGE_NAME for every component:
291
- * "mikro" for the core runtime, and the owning npm package name for a discovered
292
- * driver/board package (assigned authoritatively from the dependency graph, not
293
- * self-declared). A native module must then be named "native:<MIK_PACKAGE_NAME>/…"
294
- * and a registered builtin "<MIK_PACKAGE_NAME>/…". This reserves the native:
295
- * namespace to mikro and stops a package from claiming or shadowing another's
296
- * name. Enforced at compile time; the static_assert needs string-literal args
290
+ /* Namespace governance. Each component's CMakeLists.txt defines MIK_PACKAGE_NAME:
291
+ * "mikro" for the core runtime, the owning npm package name for a driver or
292
+ * board package. A native module must then be named "native:<MIK_PACKAGE_NAME>/…",
293
+ * and a public module "<MIK_PACKAGE_NAME>" (the package's root export) or
294
+ * "<MIK_PACKAGE_NAME>/…". This keeps the native: namespace to mikro and catches
295
+ * a module named for the wrong package; the name is self-declared, so it does
296
+ * not stop a package that declares another's.
297
+ * An app's own module, which the app imports with a "#" specifier from its
298
+ * package.json "imports", belongs to no package: its component defines no
299
+ * MIK_PACKAGE_NAME, and its public module name must start with "#".
300
+ * Enforced at compile time; the static_assert needs string-literal args
297
301
  * (always the case here). */
298
302
  #ifdef MIK_PACKAGE_NAME
299
303
  #define MIK__REQUIRE_PREFIX(name_, prefix_) \
300
304
  static_assert(__builtin_strncmp((name_), (prefix_), sizeof(prefix_) - 1) == 0, \
301
305
  "mikrojs: name must start with \"" prefix_ "\"")
302
306
  #define MIK__REQUIRE_NATIVE_NS(name_) MIK__REQUIRE_PREFIX(name_, "native:" MIK_PACKAGE_NAME "/")
303
- #define MIK__REQUIRE_BUILTIN_NS(name_) MIK__REQUIRE_PREFIX(name_, MIK_PACKAGE_NAME "/")
307
+ /* The package name itself, or a subpath of it. sizeof compares the lengths, so
308
+ * "@acme/pi" does not pass for "@acme/pix". */
309
+ #define MIK__REQUIRE_BUILTIN_NS(name_) \
310
+ static_assert((sizeof(name_) == sizeof(MIK_PACKAGE_NAME) && \
311
+ __builtin_strncmp((name_), MIK_PACKAGE_NAME, sizeof(name_)) == 0) || \
312
+ __builtin_strncmp((name_), MIK_PACKAGE_NAME "/", \
313
+ sizeof(MIK_PACKAGE_NAME "/") - 1) == 0, \
314
+ "mikrojs: name must be \"" MIK_PACKAGE_NAME "\" or start with \"" \
315
+ MIK_PACKAGE_NAME "/\"")
316
+ #define MIK__REQUIRE_PACKAGE_NS(name_) MIK__REQUIRE_BUILTIN_NS(name_)
304
317
  #else
305
318
  #define MIK__REQUIRE_PREFIX(name_, prefix_) static_assert(true, "")
306
319
  #define MIK__REQUIRE_NATIVE_NS(name_) static_assert(true, "")
307
320
  #define MIK__REQUIRE_BUILTIN_NS(name_) static_assert(true, "")
321
+ /* Core may build without MIK_PACKAGE_NAME (host builds); a package's module
322
+ * never does, or it could take any name, a core one included. Without it, a
323
+ * public module is an app's own, named "#…". */
324
+ #define MIK__REQUIRE_PACKAGE_NS(name_) \
325
+ static_assert(__builtin_strncmp((name_), "#", 1) == 0, \
326
+ "mikrojs: MIK_REGISTER_PUBLIC_MODULE needs MIK_PACKAGE_NAME, or a " \
327
+ "\"#\" name for an app's own module; define MIK_PACKAGE_NAME in " \
328
+ "the component's CMakeLists.txt")
308
329
  #endif
309
330
 
310
331
  /* The descriptor has external linkage so the linker can be told to keep it
@@ -312,6 +333,14 @@ extern mik_module_desc_t* mik__module_registry_head;
312
333
  #define MIK_REGISTER_MODULE(id, name_, init_, consume_, destroy_) \
313
334
  MIK__REQUIRE_NATIVE_NS(name_); \
314
335
  MIK__MODULE_DESC(id, name_, init_, consume_, destroy_)
336
+ /* A package's C module under its public import specifier
337
+ * ("@mikrojs/drivers/sh8601"): what app code imports, with no JS layer and no
338
+ * native: name. The specifier must start with the owning package's name, or
339
+ * with "#" for an app's own module, and the module must validate its
340
+ * arguments: app code calls it directly. */
341
+ #define MIK_REGISTER_PUBLIC_MODULE(id, name_, init_, consume_, destroy_) \
342
+ MIK__REQUIRE_PACKAGE_NS(name_); \
343
+ MIK__MODULE_DESC(id, name_, init_, consume_, destroy_)
315
344
  /* A platform C module under its public mikro/ name (mikro/gpio on ESP-IDF), for
316
345
  * modules with no bytecode layer. Core runtime only. */
317
346
  #define MIK__REGISTER_PUBLIC_MODULE(id, name_, init_, consume_, destroy_) \
@@ -325,27 +354,6 @@ extern mik_module_desc_t* mik__module_registry_head;
325
354
  mik__mod_desc_##id.next = mik__module_registry_head; \
326
355
  mik__module_registry_head = &mik__mod_desc_##id; \
327
356
  }
328
- /* Self-registration for bytecode builtins (board/driver JS runtime modules).
329
- * Driver packages use MIK_REGISTER_BUILTIN() to embed their JS wrappers as
330
- * firmware builtins, resolved by the real npm package name (e.g. "@mikrojs/your-driver"). */
331
- typedef struct mik_ext_builtin_t {
332
- const char* name; /* module name, e.g. "@mikrojs/your-driver" */
333
- const uint8_t* data; /* compiled bytecode */
334
- uint32_t data_size;
335
- struct mik_ext_builtin_t* next;
336
- } mik_ext_builtin_t;
337
-
338
- extern mik_ext_builtin_t* mik__ext_builtin_head;
339
-
340
- /* The descriptor has external linkage so the linker can keep it via -u flags. */
341
- #define MIK_REGISTER_BUILTIN(id, name_, data_, size_) \
342
- MIK__REQUIRE_BUILTIN_NS(name_); \
343
- mik_ext_builtin_t mik__builtin_##id = { \
344
- name_, data_, size_, NULL}; \
345
- static void __attribute__((constructor)) mik__register_builtin_##id(void) { \
346
- mik__builtin_##id.next = mik__ext_builtin_head; \
347
- mik__ext_builtin_head = &mik__builtin_##id; \
348
- }
349
357
  /* NOLINTEND(bugprone-macro-parentheses,cppcoreguidelines-avoid-non-const-global-variables) */
350
358
 
351
359
  /* REPL (mik_repl.cpp) */
@@ -470,6 +478,11 @@ bool MIK_ClaimGpio(int gpio, const char* owner);
470
478
  void MIK_ReleaseGpio(int gpio, const char* owner);
471
479
  /* The current owner of `gpio`, or NULL when it is free. */
472
480
  const char* MIK_GpioOwner(int gpio);
481
+ /* Claims every GPIO in `gpios` for `owner`. If one is already held, releases
482
+ * the ones this call claimed and returns an err Result carrying GpioInUse
483
+ * {name, owner, message}. Returns JS_UNDEFINED when all are claimed. */
484
+ JSValue MIK_ClaimGpios(JSContext* ctx, const int* gpios, int count, const char* owner);
485
+ void MIK_ReleaseGpios(const int* gpios, int count, const char* owner);
473
486
 
474
487
  /* A handle that owns a resource lives until its end() or close(), even when
475
488
  * the app no longer refers to it. Call MIK_KeepHandle when you create the
@@ -479,6 +492,15 @@ const char* MIK_GpioOwner(int gpio);
479
492
  void MIK_KeepHandle(JSContext* ctx, JSValueConst handle);
480
493
  void MIK_DropHandle(JSContext* ctx, JSValueConst handle);
481
494
 
495
+ /* Results for native module functions: {ok: true, value}, {ok: true}, or
496
+ * {ok: false, error: {name, message}}, with the methods app code expects
497
+ * (.map, .orPanic and the rest). MIK_ResultOk takes ownership of `value`.
498
+ * The error message is printf-style. */
499
+ JSValue MIK_ResultOk(JSContext* ctx, JSValue value);
500
+ JSValue MIK_ResultOkVoid(JSContext* ctx);
501
+ JSValue MIK_ResultErrNamed(JSContext* ctx, const char* name, const char* fmt, ...)
502
+ __attribute__((format(printf, 3, 4)));
503
+
482
504
  #ifdef __cplusplus
483
505
  }
484
506
  #endif
@@ -47,7 +47,7 @@ typedef struct MIKDeviceIdentity {
47
47
  char firmware_version[32];
48
48
  char firmware_hash[65];
49
49
  int bytecode_version;
50
- char board[48]; /* board name of this build, or empty string if unknown */
50
+ char board[64]; /* board name of this build, or empty string if unknown */
51
51
  } MIKDeviceIdentity;
52
52
 
53
53
  /* Diagnostic info from reconcile */
@@ -64,10 +64,13 @@ typedef struct MIKOtaReconcileOutcome {
64
64
  MIKOtaDiagnostic diagnostic;
65
65
  } MIKOtaReconcileOutcome;
66
66
 
67
+ /* The registry caps an app version at 64 characters; plus the NUL. */
68
+ #define MIK_OTA_VERSION_MAX 65
69
+
67
70
  /* Running build info */
68
71
  typedef struct MIKOtaRunningBuild {
69
- char checksum[65]; /* SHA-256 hex or empty string if none */
70
- char version[32]; /* package.json version or empty string if none */
72
+ char checksum[65]; /* SHA-256 hex or empty string if none */
73
+ char version[MIK_OTA_VERSION_MAX]; /* package.json version or empty string if none */
71
74
  bool trial;
72
75
  } MIKOtaRunningBuild;
73
76
 
@@ -114,9 +117,9 @@ typedef struct MIKOtaConfigErrorReport {
114
117
 
115
118
  /* Stored config document representation */
116
119
  typedef struct MIKOtaStoredConfig {
117
- char rev[MIK_OTA_REV_MAX]; /* empty string if none */
118
- char version[32]; /* required */
119
- uint8_t* doc_cbor; /* CBOR-encoded document bytes, or NULL if absent/clear */
120
+ char rev[MIK_OTA_REV_MAX]; /* empty string if none */
121
+ char version[MIK_OTA_VERSION_MAX]; /* required */
122
+ uint8_t* doc_cbor; /* CBOR-encoded document bytes, or NULL if absent/clear */
120
123
  size_t doc_cbor_len;
121
124
  } MIKOtaStoredConfig;
122
125
 
@@ -69,8 +69,8 @@ typedef struct MIKPlatform {
69
69
  int (*stdout_write)(const void* buf, size_t len);
70
70
  int (*stderr_write)(const void* buf, size_t len);
71
71
  int (*stdin_read)(void* buf, size_t len);
72
- /** Return a unique device identifier string, or NULL if unavailable.
73
- * On ESP32 this is the base MAC address encoded as Crockford's Base32
72
+ /** Required. Return the device's unique identifier: a non-empty string,
73
+ * never NULL. On ESP32 this is the base MAC address encoded as Crockford's Base32
74
74
  * (10 lowercase chars). The encoding is lossless: decoding yields the
75
75
  * original 6 MAC bytes. The returned pointer must remain valid for the
76
76
  * lifetime of the platform. */
@@ -323,12 +323,6 @@ JSModuleDef* mik__observable_operators_load(JSContext* ctx);
323
323
  int mik__observable_multicast_new(JSContext* ctx, JSValue* observable, JSValue* next,
324
324
  JSValue* complete);
325
325
 
326
- /* GPIO claims (gpio_claim.cpp). Claims every GPIO for `owner`; on the first one
327
- * already held, releases those claimed so far and returns an err Result
328
- * carrying GpioInUse {name, owner, message}. Returns JS_UNDEFINED on success. */
329
- JSValue mik__claim_gpios(JSContext* ctx, const int* gpios, int count, const char* owner);
330
- void mik__release_gpios(const int* gpios, int count, const char* owner);
331
-
332
326
  /* Watchdog (mik_watchdog.cpp). */
333
327
  /* Start a fresh blocking budget: top of each MIK_Loop pass, eval entry
334
328
  * points, REPL eval, and on return from a deliberately blocking native
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.21.0",
3
+ "version": "0.21.1-next.20260926222438",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -24,6 +24,7 @@
24
24
  "prebuilds",
25
25
  "cmake",
26
26
  "cmake.js",
27
+ "cmake.d.ts",
27
28
  "CMakeLists.txt",
28
29
  "deps/nanocbor/src",
29
30
  "deps/nanocbor/include",
@@ -85,14 +86,14 @@
85
86
  "./runtime/wifi/types": "./runtime/wifi/types.ts"
86
87
  },
87
88
  "dependencies": {
88
- "@mikrojs/quickjs": "0.21.0",
89
+ "@mikrojs/quickjs": "0.21.1-next.20260926222438+5c06152",
89
90
  "cmake-js": "^8.0.0",
90
91
  "node-addon-api": "^8.9.2",
91
92
  "node-gyp-build": "^4.8.4"
92
93
  },
93
94
  "devDependencies": {
94
- "@mikrojs/registry": "0.21.0",
95
- "@mikrojs/schema": "0.21.0",
95
+ "@mikrojs/registry": "0.21.1-next.20260926222438+5c06152",
96
+ "@mikrojs/schema": "0.21.1-next.20260926222438+5c06152",
96
97
  "@swc/core": "^1.16.2",
97
98
  "@types/node": "^24.12.2",
98
99
  "esbuild": "^0.28.2",
@@ -87,11 +87,12 @@ export function makeCreateValue(native: NativeKvFns) {
87
87
  return v
88
88
  }
89
89
 
90
+ function doDelete() {
91
+ return native.remove(key) ? ok() : err(KVError.WriteFailed(`failed to delete key "${key}"`))
92
+ }
93
+
90
94
  function doSet(value: unknown) {
91
- if (value === undefined) {
92
- native.remove(key)
93
- return ok(undefined)
94
- }
95
+ if (value === undefined) return doDelete()
95
96
  if (schema) {
96
97
  const result = parse(schema, value)
97
98
  if (!result.ok) {
@@ -109,10 +110,7 @@ export function makeCreateValue(native: NativeKvFns) {
109
110
  update(updater: (value: any) => any) {
110
111
  return doSet(updater(doGet()))
111
112
  },
112
- delete() {
113
- const removed = native.remove(key)
114
- return removed ? ok() : err(KVError.WriteFailed(`failed to delete key "${key}"`))
115
- },
113
+ delete: doDelete,
116
114
  }
117
115
  }
118
116
  }
@@ -108,7 +108,7 @@ for (const name of moduleNames) {
108
108
  target: 'es2024',
109
109
  platform: 'neutral',
110
110
  format: 'esm',
111
- external: ['mikro', 'mikro/*', '@mikrojs/*', 'native:*'],
111
+ external: ['mikro', 'mikro/*', 'native:*'],
112
112
  metafile: true,
113
113
  })
114
114
 
@@ -141,9 +141,7 @@ for (const name of moduleNames) {
141
141
 
142
142
  // Extract external imports so CMake can pass them as -M flags to qjsc
143
143
  const externals = []
144
- for (const match of source.matchAll(
145
- /(?:from|import)\s*["']((?:native:|mikro\/|@mikrojs\/)[^"']+)["']/g,
146
- )) {
144
+ for (const match of source.matchAll(/(?:from|import)\s*["']((?:native:|mikro\/)[^"']+)["']/g)) {
147
145
  externals.push(match[1])
148
146
  }
149
147
 
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Print builtin module lists derived from runtime/modules.json for CMake.
3
+ * Print the builtin module lists derived from runtime/modules.json for CMake.
4
4
  *
5
- * Usage: node modules-list.js <bytecode|native|features> [--host] [--ble=on|off] [--wifi=on|off]
5
+ * Usage: node modules-list.js [--host] [--ble=on|off] [--wifi=on|off]
6
6
  *
7
- * - `bytecode`: bytecode module names (mikrojs_generate_bytecode MODULES)
8
- * - `native`: native module ids (mikrojs_force_include_modules)
9
- * - `features`: firmware features present in the build (for MIK_FW_FEATURES)
10
7
  * - `--host`: the host/sim build: every module, no compile gating
11
8
  * - `--ble`, `--wifi`: device builds only; a module whose `when` names the
12
9
  * gate is included iff it is `on`
13
10
  *
14
- * Output is semicolon-separated (a CMake list) on stdout.
11
+ * Prints one JSON object on stdout, each value a semicolon-separated CMake list:
12
+ * - `bytecode`: bytecode module names (mikrojs_generate_bytecode MODULES)
13
+ * - `native`: native module ids (mikrojs_force_include_modules)
14
+ * - `features`: firmware features present in the build (for MIK_FW_FEATURES)
15
15
  *
16
16
  * Fields of a runtime/modules.json entry:
17
17
  * - `name`: the module name; the import specifier is `mikro/<name>`
@@ -32,19 +32,12 @@ import {fileURLToPath} from 'node:url'
32
32
  const modulesJsonPath = fileURLToPath(new URL('../runtime/modules.json', import.meta.url))
33
33
  const {modules} = JSON.parse(readFileSync(modulesJsonPath, 'utf8'))
34
34
 
35
- const kind = process.argv[2]
36
35
  const host = process.argv.includes('--host')
37
36
  const gates = ['ble', 'wifi']
38
37
  const flags = Object.fromEntries(
39
38
  gates.map((g) => [g, process.argv.find((a) => a.startsWith(`--${g}=`))]),
40
39
  )
41
40
 
42
- if (kind !== 'bytecode' && kind !== 'native' && kind !== 'features') {
43
- process.stderr.write(
44
- 'usage: modules-list.js <bytecode|native|features> [--host] [--ble=on|off] [--wifi=on|off]\n',
45
- )
46
- process.exit(1)
47
- }
48
41
  if (!host && gates.some((g) => !flags[g])) {
49
42
  process.stderr.write('modules-list.js: device builds must pass --ble=on|off and --wifi=on|off\n')
50
43
  process.exit(1)
@@ -52,17 +45,20 @@ if (!host && gates.some((g) => !flags[g])) {
52
45
 
53
46
  const off = new Set(host ? [] : gates.filter((g) => flags[g] !== `--${g}=on`))
54
47
  const included = modules.filter((m) => !m.when || !off.has(m.when))
55
- const names =
56
- kind === 'bytecode'
57
- ? included.filter((m) => m.bytecode !== false).map((m) => m.name)
58
- : kind === 'native'
59
- ? included.filter((m) => m.native).map((m) => m.native)
60
- : // A module can need a feature without being compiled under its gate
61
- // (http/server needs wifi and always compiles), so a feature whose
62
- // gate is off is dropped here. This relies on each gated feature
63
- // having the same name as its gate.
64
- [...new Set(included.filter((m) => m.feature).map((m) => m.feature))].filter(
65
- (f) => !off.has(f),
66
- )
48
+ const bytecode = included.filter((m) => m.bytecode !== false).map((m) => m.name)
49
+ const native = included.filter((m) => m.native).map((m) => m.native)
50
+ // A module can need a feature without being compiled under its gate
51
+ // (http/server needs wifi and always compiles), so a feature whose gate is
52
+ // off is dropped here. This relies on each gated feature having the same
53
+ // name as its gate.
54
+ const features = [...new Set(included.filter((m) => m.feature).map((m) => m.feature))].filter(
55
+ (f) => !off.has(f),
56
+ )
67
57
 
68
- process.stdout.write(names.join(';'))
58
+ process.stdout.write(
59
+ JSON.stringify({
60
+ bytecode: bytecode.join(';'),
61
+ native: native.join(';'),
62
+ features: features.join(';'),
63
+ }),
64
+ )
package/src/builtins.cpp CHANGED
@@ -15,13 +15,8 @@ typedef struct {
15
15
  /* Generated by mikrojs_generate_bytecode() in CMake — includes + lookup table.
16
16
  * Adding a new module only requires updating the MODULES list in CMakeLists.txt. */
17
17
  #include "gen/mikro_builtins_table.h"
18
- #define builtins mikro_builtins
19
18
 
20
- /* External builtins registered by board/driver packages via MIK_REGISTER_BUILTIN() */
21
- // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
22
- mik_ext_builtin_t* mik__ext_builtin_head = nullptr;
23
-
24
- /* Deserialize bytecode into a module. Shared by core and external builtins. */
19
+ /* Deserialize bytecode into a module. */
25
20
  static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
26
21
  const uint8_t* data, uint32_t data_size) {
27
22
  const MIKPlatform* platform = MIK_GetPlatform();
@@ -68,15 +63,7 @@ static JSModuleDef* mik__deserialize_builtin(JSContext* ctx, const char* name,
68
63
  }
69
64
 
70
65
  JSModuleDef* mik__load_builtin(JSContext* ctx, const char* name) {
71
- /* Check core builtins table */
72
- for (const mik_builtin_t* p = builtins; p->name != NULL; ++p) {
73
- if (strcmp(p->name, name) == 0) {
74
- return mik__deserialize_builtin(ctx, name, p->data, p->data_size);
75
- }
76
- }
77
-
78
- /* Check external builtins (registered by board/driver packages) */
79
- for (mik_ext_builtin_t* p = mik__ext_builtin_head; p != nullptr; p = p->next) {
66
+ for (const mik_builtin_t* p = mikro_builtins; p->name != NULL; ++p) {
80
67
  if (strcmp(p->name, name) == 0) {
81
68
  return mik__deserialize_builtin(ctx, name, p->data, p->data_size);
82
69
  }
@@ -42,7 +42,7 @@ static std::string mik__gpio_in_use_message(int gpio, const char* owner) {
42
42
  return message + owner;
43
43
  }
44
44
 
45
- JSValue mik__claim_gpios(JSContext* ctx, const int* gpios, int count, const char* owner) {
45
+ JSValue MIK_ClaimGpios(JSContext* ctx, const int* gpios, int count, const char* owner) {
46
46
  for (int i = 0; i < count; i++) {
47
47
  if (MIK_ClaimGpio(gpios[i], owner)) continue;
48
48
  const char* holder = MIK_GpioOwner(gpios[i]);
@@ -52,12 +52,12 @@ JSValue mik__claim_gpios(JSContext* ctx, const int* gpios, int count, const char
52
52
  std::string message = mik__gpio_in_use_message(gpios[i], holder);
53
53
  JS_SetPropertyStr(ctx, error, "message",
54
54
  JS_NewStringLen(ctx, message.data(), message.size()));
55
- mik__release_gpios(gpios, i, owner);
55
+ MIK_ReleaseGpios(gpios, i, owner);
56
56
  return mik__result_err_obj(ctx, error);
57
57
  }
58
58
  return JS_UNDEFINED;
59
59
  }
60
60
 
61
- void mik__release_gpios(const int* gpios, int count, const char* owner) {
61
+ void MIK_ReleaseGpios(const int* gpios, int count, const char* owner) {
62
62
  for (int i = 0; i < count; i++) MIK_ReleaseGpio(gpios[i], owner);
63
63
  }
@@ -231,7 +231,7 @@ JSValue MIKOtaConfigReader::LoadDoc(JSContext* ctx, bool* out_failed) {
231
231
  return JS_UNDEFINED;
232
232
  }
233
233
 
234
- char version[32] = {};
234
+ char version[MIK_OTA_VERSION_MAX] = {};
235
235
  if (!env_ || !env_->read_app_version ||
236
236
  !env_->read_app_version(env_->opaque, version, sizeof(version))) {
237
237
  return JS_UNDEFINED;
@@ -398,7 +398,7 @@ MIKOtaConfigWrite mik__ota_deliver_config(const MIKOtaEnv* env, const MIKOtaStor
398
398
  int trial_boots) {
399
399
  if (!config) return MIKOtaConfigWrite::kUnchanged;
400
400
 
401
- char running[32] = {};
401
+ char running[MIK_OTA_VERSION_MAX] = {};
402
402
  if (!env || !env->read_app_version ||
403
403
  !env->read_app_version(env->opaque, running, sizeof(running))) {
404
404
  // The version the document has to be matched against could not be read,
package/src/mik_repl.cpp CHANGED
@@ -34,9 +34,10 @@ static bool repl_async_skipped = false; /* set when eval bails on paused async
34
34
  static MIKReplTransport* repl_transport = nullptr;
35
35
  /* Sized to hold the base map plus a maximum-length npm package name as the
36
36
  * `fw` identity (214 chars), with room to spare for a typical `[rev, name]`
37
- * pair. A max-length identity combined with a near-max device name can still
37
+ * pair, or a typical map with about ten package native modules (~30 bytes
38
+ * each). A max-length identity combined with a near-max device name can still
38
39
  * exceed this; refresh_ready then drops the name (never the identity). */
39
- static uint8_t ready_buf[384];
40
+ static uint8_t ready_buf[512];
40
41
  static size_t ready_len = 0;
41
42
 
42
43
  /* Memory left for the app, captured once from the boot path (see
@@ -474,10 +475,7 @@ static bool handle_directive_impl(JSContext* ctx, const char* line, std::string&
474
475
  if (repl_transport && repl_transport->chip_name) {
475
476
  dir_printf(out, "Chip: %s\n", repl_transport->chip_name);
476
477
  }
477
- const char* dev_id = MIK_GetPlatform()->get_device_id();
478
- if (dev_id) {
479
- dir_printf(out, "Device ID: %s\n", dev_id);
480
- }
478
+ dir_printf(out, "Device ID: %s\n", MIK_GetPlatform()->get_device_id());
481
479
 
482
480
  /* Runtime uptime */
483
481
  int64_t uptime_us = platform->get_boot_us();
@@ -906,15 +904,19 @@ static std::vector<uint8_t> proto_complete(JSContext* ctx, const char* partial,
906
904
 
907
905
  /* Fills ready_buf/ready_len with CBOR device info:
908
906
  * {"chip": tstr, "id": tstr, "v": tstr, "board": tstr, "fw": tstr (when built
909
- * with MIK_FW_NAME), "name": tstr (only when named), "features": [tstr] (when
910
- * built with MIK_FW_FEATURES; host builds omit it)}.
907
+ * with MIK_BOARD_NAME), "name": tstr (only when named), "features": [tstr] (when
908
+ * built with MIK_FW_FEATURES; host builds omit it), "natives": [tstr]}.
911
909
  * `board` is mik__board_name(). `features` lists the firmware features the
912
910
  * build compiled in (from the comma-separated MIK_FW_FEATURES define), so the
913
- * host knows which feature-gated builtins this firmware carries.
914
- * `fw` is the firmware identity (the firmware project's package name). The
915
- * host only auto-flashes its bundled prebuilt over a device whose identity
916
- * matches that prebuilt; omitting it reads as firmware predating identity
917
- * reporting, which the host treats as its own bundled firmware.
911
+ * host knows which feature-gated builtins this firmware carries. `natives`
912
+ * lists the registered C modules outside the core namespace (package native
913
+ * modules compiled in, e.g. "@mikrojs/drivers/sh8601"; empty on a generic
914
+ * build), so the host can refuse an app importing one this firmware lacks.
915
+ * `fw` is the firmware identity: the board name the build set (MIK_BOARD_NAME),
916
+ * which `board` also reports. The host only auto-flashes its bundled image over
917
+ * a device whose identity matches that image's name; omitting it reads as
918
+ * firmware predating identity reporting, which the host treats as its own
919
+ * bundled firmware.
918
920
  * `name` carries the raw `[rev, name]` pair from mik.sys, so the host reads the
919
921
  * name and its revision together and can never see them out of step. Omitted
920
922
  * entirely when the device has never been named, which the host reads as
@@ -930,18 +932,25 @@ static std::vector<uint8_t> proto_complete(JSContext* ctx, const char* partial,
930
932
  * the runtime is new (see there). */
931
933
  static void refresh_ready(MIKReplTransport* transport);
932
934
 
935
+ /* A package's native module, under the public specifier apps import
936
+ * ("@mikrojs/drivers/sh8601"). native: names are internal, and the host only
937
+ * compares public specifiers, so they would only take buffer space. */
938
+ static bool is_package_module(const char* name) {
939
+ return strncmp(name, "native:", 7) != 0 && strncmp(name, "mikro/", 6) != 0;
940
+ }
941
+
933
942
  /* Encodes the MSG_READY map into buf, or measures it when buf is null. Returns
934
943
  * the length the encoding needs, which exceeds cap when it did not fit. */
935
944
  static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const char* id,
936
945
  const char* version, const char* board, const char* fw,
937
- const char* name, const char* features, bool mem) {
946
+ const char* name, const char* features, bool natives, bool mem) {
938
947
  /* Measuring calls pass buf=NULL; substitute a non-null base so
939
948
  * zero-length appends never hand memcpy a null pointer (UB). */
940
949
  static uint8_t measure_base;
941
950
  nanocbor_encoder_t enc;
942
951
  nanocbor_encoder_init(&enc, buf ? buf : &measure_base, buf ? cap : 0);
943
- nanocbor_fmt_map(&enc,
944
- 4 + (fw ? 1 : 0) + (name ? 1 : 0) + (features ? 1 : 0) + (mem ? 3 : 0));
952
+ nanocbor_fmt_map(&enc, 4 + (fw ? 1 : 0) + (name ? 1 : 0) + (features ? 1 : 0) +
953
+ (natives ? 1 : 0) + (mem ? 3 : 0));
945
954
  nanocbor_put_tstr(&enc, "chip");
946
955
  nanocbor_put_tstr(&enc, chip);
947
956
  nanocbor_put_tstr(&enc, "id");
@@ -977,6 +986,17 @@ static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const cha
977
986
  p = comma ? comma + 1 : p + len;
978
987
  }
979
988
  }
989
+ if (natives) {
990
+ size_t count = 0;
991
+ for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
992
+ if (is_package_module(d->name)) count++;
993
+ }
994
+ nanocbor_put_tstr(&enc, "natives");
995
+ nanocbor_fmt_array(&enc, count);
996
+ for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
997
+ if (is_package_module(d->name)) nanocbor_put_tstr(&enc, d->name);
998
+ }
999
+ }
980
1000
  if (mem) {
981
1001
  nanocbor_put_tstr(&enc, "heapFree");
982
1002
  nanocbor_fmt_uint(&enc, boot_heap_free);
@@ -991,7 +1011,6 @@ static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const cha
991
1011
  static void refresh_ready(MIKReplTransport* transport) {
992
1012
  const char* chip = transport->chip_name ? transport->chip_name : "unknown";
993
1013
  const char* id = MIK_GetPlatform()->get_device_id();
994
- if (!id) id = "";
995
1014
  const char* version =
996
1015
  #ifdef MIK_FW_VERSION
997
1016
  MIK_FW_VERSION;
@@ -999,8 +1018,8 @@ static void refresh_ready(MIKReplTransport* transport) {
999
1018
  "0.0.0-dev";
1000
1019
  #endif
1001
1020
  const char* fw =
1002
- #ifdef MIK_FW_NAME
1003
- MIK_FW_NAME;
1021
+ #ifdef MIK_BOARD_NAME
1022
+ MIK_BOARD_NAME;
1004
1023
  #else
1005
1024
  nullptr;
1006
1025
  #endif
@@ -1017,34 +1036,40 @@ static void refresh_ready(MIKReplTransport* transport) {
1017
1036
  * end but still reports the length it would have needed, so ready_len must
1018
1037
  * come from a run that actually fit: sending the measured length would read
1019
1038
  * past ready_buf. Fields go in order of how little losing them costs.
1020
- * `features` first: without it the host treats the firmware as legacy and
1039
+ * The memory figures first (below). Then `natives`: without it the host
1040
+ * skips the native module gate, as it does for firmware that predates it.
1041
+ * Then `features`: without it the host treats the firmware as legacy and
1021
1042
  * skips the feature gate, which is how every older firmware behaves. Then
1022
1043
  * `name`, dropped whole rather than truncated, since a partial pair decodes
1023
1044
  * as a different name at the host. `fw` last: a dropped `fw` reads as the
1024
1045
  * host's own bundled firmware, re-enabling the auto-reflash the identity
1025
1046
  * exists to prevent. `board` and the core fields are never dropped. */
1026
1047
  bool mem = boot_mem_captured;
1048
+ bool natives = true;
1027
1049
  /* Diagnostics are the first thing to go when the map does not fit: losing
1028
1050
  * them costs a memory figure, while losing `name` or `fw` breaks identity. */
1029
- if (mem && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, mem) >
1030
- sizeof(ready_buf)) {
1051
+ if (mem && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, natives,
1052
+ mem) > sizeof(ready_buf)) {
1031
1053
  mem = false;
1032
1054
  }
1033
- if (features &&
1034
- encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, mem) >
1035
- sizeof(ready_buf)) {
1055
+ if (natives && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features,
1056
+ natives, mem) > sizeof(ready_buf)) {
1057
+ natives = false;
1058
+ }
1059
+ if (features && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features,
1060
+ natives, mem) > sizeof(ready_buf)) {
1036
1061
  features = nullptr;
1037
1062
  }
1038
- if (name && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, mem) >
1039
- sizeof(ready_buf)) {
1063
+ if (name && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, natives,
1064
+ mem) > sizeof(ready_buf)) {
1040
1065
  name = nullptr;
1041
1066
  }
1042
- if (fw && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, mem) >
1043
- sizeof(ready_buf)) {
1067
+ if (fw && encode_ready(nullptr, 0, chip, id, version, board, fw, name, features, natives,
1068
+ mem) > sizeof(ready_buf)) {
1044
1069
  fw = nullptr;
1045
1070
  }
1046
1071
  ready_len = encode_ready(ready_buf, sizeof(ready_buf), chip, id, version, board, fw, name,
1047
- features, mem);
1072
+ features, natives, mem);
1048
1073
  if (ready_len > sizeof(ready_buf)) ready_len = 0;
1049
1074
  }
1050
1075
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <quickjs.h>
5
5
 
6
+ #include "mikrojs/mikrojs.h"
6
7
  #include "mikrojs/private.h"
7
8
  #include "mikrojs/utils.h"
8
9
 
@@ -83,14 +84,12 @@ JSValue mik__result_err(JSContext* ctx, int code, int platform_errno, const char
83
84
  return obj;
84
85
  }
85
86
 
86
- JSValue mik__result_err_named(JSContext* ctx, const char* name, const char* fmt, ...) {
87
- va_list ap;
88
- va_start(ap, fmt);
87
+ static JSValue result_err_named_v(JSContext* ctx, const char* name, const char* fmt,
88
+ va_list ap) {
89
89
  /* 384 matches the HTTP task's error_buf: fetch errors pass through here
90
90
  * verbatim, and a smaller buffer truncates their trailing heap figures. */
91
91
  char msg[384];
92
92
  vsnprintf(msg, sizeof(msg), fmt, ap);
93
- va_end(ap);
94
93
 
95
94
  JSValue error = JS_NewObject(ctx);
96
95
  JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, name), JS_PROP_C_W_E);
@@ -102,6 +101,26 @@ JSValue mik__result_err_named(JSContext* ctx, const char* name, const char* fmt,
102
101
  return obj;
103
102
  }
104
103
 
104
+ JSValue mik__result_err_named(JSContext* ctx, const char* name, const char* fmt, ...) {
105
+ va_list ap;
106
+ va_start(ap, fmt);
107
+ JSValue result = result_err_named_v(ctx, name, fmt, ap);
108
+ va_end(ap);
109
+ return result;
110
+ }
111
+
112
+ JSValue MIK_ResultOk(JSContext* ctx, JSValue value) { return mik__result_ok(ctx, value); }
113
+
114
+ JSValue MIK_ResultOkVoid(JSContext* ctx) { return mik__result_ok_void(ctx); }
115
+
116
+ JSValue MIK_ResultErrNamed(JSContext* ctx, const char* name, const char* fmt, ...) {
117
+ va_list ap;
118
+ va_start(ap, fmt);
119
+ JSValue result = result_err_named_v(ctx, name, fmt, ap);
120
+ va_end(ap);
121
+ return result;
122
+ }
123
+
105
124
  JSValue mik__result_err_tag(JSContext* ctx, const char* name) {
106
125
  JSValue error = JS_NewObject(ctx);
107
126
  JS_DefinePropertyValueStr(ctx, error, "name", JS_NewString(ctx, name), JS_PROP_C_W_E);
package/src/mik_sys.cpp CHANGED
@@ -333,20 +333,18 @@ void mik__sys_api_init(JSContext* ctx, JSValue ns) {
333
333
  JS_SetPropertyStr(ctx, ns, "board", mik__sys_board(ctx));
334
334
  JS_SetPropertyStr(ctx, ns, "firmware", mik__sys_firmware(ctx));
335
335
 
336
- /* Guard the platform function POINTERS, not just their results: a
337
- * platform that omits an optional hook leaves a NULL pointer, and
338
- * calling it is a segfault with no diagnostics (the sim was down for
339
- * weeks because platform_node lacked get_reset_reason). */
340
336
  const MIKPlatform* platform = MIK_GetPlatform();
341
- const char* device_id = platform->get_device_id ? platform->get_device_id() : NULL;
342
- JS_SetPropertyStr(ctx, ns, "deviceId",
343
- device_id ? JS_NewString(ctx, device_id) : JS_UNDEFINED);
337
+ JS_SetPropertyStr(ctx, ns, "deviceId", JS_NewString(ctx, platform->get_device_id()));
344
338
 
345
339
  JS_SetPropertyStr(ctx, ns, "deviceName",
346
340
  JS_NewCFunction(ctx, mik__sys_device_name, "deviceName", 0));
347
341
  JS_SetPropertyStr(ctx, ns, "setDeviceName",
348
342
  JS_NewCFunction(ctx, mik__sys_set_device_name, "setDeviceName", 1));
349
343
 
344
+ /* Guard an optional hook's POINTER, not just its result: a platform that
345
+ * omits it leaves a NULL pointer, and calling it is a segfault with no
346
+ * diagnostics (the sim was down for weeks because platform_node lacked
347
+ * get_reset_reason). */
350
348
  const char* reset_reason =
351
349
  platform->get_reset_reason ? platform->get_reset_reason() : NULL;
352
350
  JS_SetPropertyStr(ctx, ns, "resetReason",
package/src/mikrojs.cpp CHANGED
@@ -611,14 +611,6 @@ static void mik__check_module_collisions(void) {
611
611
  }
612
612
  }
613
613
  }
614
- for (mik_ext_builtin_t* a = mik__ext_builtin_head; a != nullptr; a = a->next) {
615
- for (mik_ext_builtin_t* b = a->next; b != nullptr; b = b->next) {
616
- if (strcmp(a->name, b->name) == 0) {
617
- fprintf(stderr, "FATAL: builtin module name collision: \"%s\"\n", a->name);
618
- abort();
619
- }
620
- }
621
- }
622
614
  }
623
615
 
624
616
  /* A chain of already-settled promises drains as one uninterrupted storm of
@@ -756,9 +748,9 @@ int MIK_Loop(MIKRuntime* mik_rt) {
756
748
  mik__stdin_consume(mik_rt->ctx);
757
749
  mik__timers_consume(mik_rt->ctx);
758
750
 
759
- /* Call registered loop consumers */
751
+ /* Call registered loop consumers; a module may register only a destroy hook. */
760
752
  for (const auto& consumer : mik_rt->loop_consumers) {
761
- consumer.consume_fn(mik_rt->ctx);
753
+ if (consumer.consume_fn) consumer.consume_fn(mik_rt->ctx);
762
754
  }
763
755
 
764
756
  mik__execute_jobs(mik_rt->ctx);
package/src/modules.cpp CHANGED
@@ -227,7 +227,7 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
227
227
  for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
228
228
  if (strcmp(d->name, module_name) == 0) {
229
229
  JSModuleDef* lazy_m = d->init(ctx);
230
- if (lazy_m && mik_rt && d->consume) {
230
+ if (lazy_m && mik_rt && (d->consume || d->destroy)) {
231
231
  MIK_RegisterLoopConsumer(mik_rt, d->consume, d->destroy);
232
232
  }
233
233
  return lazy_m;
@@ -261,7 +261,7 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
261
261
  for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
262
262
  if (strcmp(d->name, module_name) == 0) {
263
263
  JSModuleDef* reg_m = d->init(ctx);
264
- if (reg_m && mik_rt && d->consume) {
264
+ if (reg_m && mik_rt && (d->consume || d->destroy)) {
265
265
  MIK_RegisterLoopConsumer(mik_rt, d->consume, d->destroy);
266
266
  }
267
267
  return reg_m;
@@ -282,18 +282,19 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
282
282
  return NULL;
283
283
  }
284
284
 
285
- /* Check external builtins for board/driver packages (e.g. "@mikrojs/your-driver").
286
- * These are registered via MIK_REGISTER_BUILTIN() and resolved by package name
287
- * before falling through to filesystem resolution. */
288
- {
289
- JSModuleDef* ext_m = mik__load_builtin(ctx, module_name);
290
- if (ext_m) return ext_m;
291
- /* NULL with an exception pending means the builtin was found but
292
- * failed to load (deserialization or import resolution). Don't fall
293
- * through to filesystem resolution — it would replace the real error
294
- * with a generic "failed to resolve" one. Same probe as the mikro/
295
- * branch above (JS_HasException, not JS_GetException + JS_IsNull). */
296
- if (JS_HasException(ctx)) return NULL;
285
+ /* Package C modules registered under their public import specifier
286
+ * (MIK_REGISTER_PUBLIC_MODULE, e.g. "@mikrojs/drivers/sh8601"). Filesystem
287
+ * modules are named by path, so a bare name can only be one of these. */
288
+ if (module_name[0] != '/' && module_name[0] != '.') {
289
+ for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
290
+ if (strcmp(d->name, module_name) == 0) {
291
+ JSModuleDef* pkg_m = d->init(ctx);
292
+ if (pkg_m && mik_rt && (d->consume || d->destroy)) {
293
+ MIK_RegisterLoopConsumer(mik_rt, d->consume, d->destroy);
294
+ }
295
+ return pkg_m;
296
+ }
297
+ }
297
298
  }
298
299
 
299
300
  return mik_module_load_from_fs(ctx, module_name);
@@ -450,7 +451,11 @@ int js_module_set_import_meta(JSContext* ctx, JSValue func_val, bool use_realpat
450
451
 
451
452
  /*
452
453
  * Resolve an export condition value to a string path.
453
- * Handles: string literals, and objects with "import" / "default" conditions.
454
+ * Handles: string literals, and objects with "import" / "default" / "native"
455
+ * conditions. A native module's "default" is JavaScript for hosts (tests, the
456
+ * simulator), so it wins; "native" last names the C/C++ source of a module
457
+ * without one, which is reported as missing from the firmware. A device never
458
+ * gets a native module's package files: the CLI keeps the import external.
454
459
  */
455
460
  static const char* mik__resolve_export_condition(JSContext* ctx, JSValue exp) {
456
461
  if (JS_IsString(exp)) {
@@ -459,7 +464,7 @@ static const char* mik__resolve_export_condition(JSContext* ctx, JSValue exp) {
459
464
  if (!JS_IsObject(exp)) {
460
465
  return NULL;
461
466
  }
462
- static const char* conditions[] = {"import", "default"};
467
+ static const char* conditions[] = {"import", "default", "native"};
463
468
  for (size_t i = 0; i < countof(conditions); i++) {
464
469
  JSValue val = JS_GetPropertyStr(ctx, exp, conditions[i]);
465
470
  if (!JS_IsUndefined(val) && !JS_IsException(val)) {
@@ -698,18 +703,23 @@ static bool mik__is_anchored_name(const char* name) {
698
703
  strncmp(name, "@mikrojs/", 9) == 0;
699
704
  }
700
705
 
701
- /* A name registered via MIK_REGISTER_BUILTIN() is bytecode compiled into the
702
- * firmware — the same trust level as @mikrojs builtins, regardless of the
703
- * package's npm scope. */
704
- static bool mik__is_ext_builtin_name(const char* name) {
705
- for (mik_ext_builtin_t* p = mik__ext_builtin_head; p != nullptr; p = p->next) {
706
- if (strcmp(p->name, name) == 0) {
707
- return true;
708
- }
706
+ /* A name registered via MIK_REGISTER_PUBLIC_MODULE(): a package's C module,
707
+ * imported by its bare specifier. */
708
+ static bool mik__is_public_module_name(const char* name) {
709
+ for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
710
+ if (strcmp(d->name, name) == 0) return true;
709
711
  }
710
712
  return false;
711
713
  }
712
714
 
715
+ /* A package export whose target is C/C++ source is a native module. */
716
+ static bool mik__is_native_source(const char* path) {
717
+ const char* ext = strrchr(path, '.');
718
+ if (!ext) return false;
719
+ return strcmp(ext, ".c") == 0 || strcmp(ext, ".cc") == 0 || strcmp(ext, ".cpp") == 0 ||
720
+ strcmp(ext, ".cxx") == 0;
721
+ }
722
+
713
723
  /* Record an edge in the module dependency graph: `base` imports `target`.
714
724
  * No-ops if the MIKRuntime is absent, base is empty/non-module, or target
715
725
  * is anchored. Keyed by normalized names. */
@@ -730,13 +740,9 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
730
740
 
731
741
  static const char internal_prefix[] = "native:";
732
742
  if (strncmp(name, internal_prefix, strlen(internal_prefix)) == 0) {
733
- // Only firmware-embedded modules (native:, mikro/, @mikrojs/, registered
734
- // external builtins) may import native: internals. A builtin may import
735
- // any native: module, including another package's — native-level
736
- // composition (e.g. an audio driver reusing a codec package's bindings)
737
- // is intentionally allowed.
738
- if (strncmp(base_name, "native:", 7) != 0 && strncmp(base_name, "mikro/", 6) != 0 &&
739
- strncmp(base_name, "@mikrojs/", 9) != 0 && !mik__is_ext_builtin_name(base_name)) {
743
+ // Only firmware-embedded modules (native:, mikro/) may import native:
744
+ // internals.
745
+ if (strncmp(base_name, "native:", 7) != 0 && strncmp(base_name, "mikro/", 6) != 0) {
740
746
  JS_ThrowTypeError(
741
747
  ctx, "Cannot import '%s': native modules can only be imported by firmware builtins",
742
748
  name);
@@ -749,9 +755,9 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
749
755
  if (strncmp(name, "mikro/", 6) == 0 || strncmp(name, "native:", 7) == 0) {
750
756
  return js_strdup(ctx, name);
751
757
  }
752
- /* Check if this bare specifier matches an external builtin (board/driver
753
- * package). If so, pass through unchanged to avoid filesystem resolution. */
754
- if (mik__is_ext_builtin_name(name)) {
758
+ /* A package's C module passes through too: the firmware provides it,
759
+ * so it is never looked up in node_modules. */
760
+ if (mik__is_public_module_name(name)) {
755
761
  return js_strdup(ctx, name);
756
762
  }
757
763
  /* Bare specifier — try node_modules resolution.
@@ -771,6 +777,16 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
771
777
  }
772
778
  char* resolved = mik__resolve_node_modules(ctx, base_dir, name);
773
779
  js_free(ctx, base_dir);
780
+ if (resolved && mik__is_native_source(resolved)) {
781
+ /* A registered native module never reaches node_modules, so
782
+ * this one is not in the firmware. */
783
+ js_free(ctx, resolved);
784
+ JS_ThrowTypeError(ctx,
785
+ "Cannot import '%s': this firmware was not built with that "
786
+ "native module. Flash firmware that was built with it.",
787
+ name);
788
+ return NULL;
789
+ }
774
790
  if (resolved) {
775
791
  return resolved;
776
792
  }