@mikrojs/native 0.21.0 → 0.21.1-next.20260927212749
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 +7 -6
- package/cmake/mikrojs_bytecode.cmake +13 -27
- package/cmake.d.ts +12 -0
- package/include/mikrojs/mikrojs.h +51 -29
- package/include/mikrojs/ota_env.h +9 -6
- package/include/mikrojs/platform.h +2 -2
- package/include/mikrojs/private.h +0 -6
- package/package.json +5 -4
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/kv/shared.ts +6 -8
- package/scripts/bundle-runtime.js +2 -4
- package/scripts/modules-list.js +22 -26
- package/src/builtins.cpp +2 -15
- package/src/gpio_claim.cpp +3 -3
- package/src/mik_app_config.cpp +7 -1
- package/src/mik_ota_config.cpp +2 -2
- package/src/mik_repl.cpp +55 -30
- package/src/mik_result.cpp +23 -4
- package/src/mik_sys.cpp +5 -7
- package/src/mikrojs.cpp +2 -10
- package/src/modules.cpp +50 -34
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"
|
|
99
|
-
OUTPUT_VARIABLE
|
|
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
|
|
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/
|
|
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() —
|
|
2
|
-
#
|
|
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
|
|
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;
|
|
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}
|
|
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
|
-
"
|
|
103
|
-
"${
|
|
94
|
+
"mikro/${mod}"
|
|
95
|
+
"mikro_${_mod_safe}_bytecode"
|
|
104
96
|
DEPENDS ${_BUNDLE_DIR}/bundle.stamp ${_COMPILE_SCRIPT} ${QJSC_EXECUTABLE}
|
|
105
|
-
COMMENT "Compiling bytecode:
|
|
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}
|
|
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 \"${
|
|
107
|
+
string(APPEND _TABLE_CONTENT "#include \"mikro_${_mod_safe}.h\"\n")
|
|
116
108
|
endforeach()
|
|
117
|
-
string(APPEND _TABLE_CONTENT "\nstatic const mik_builtin_t
|
|
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 " {\"
|
|
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.
|
|
291
|
-
* "mikro" for the core runtime,
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
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
|
-
|
|
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[
|
|
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];
|
|
70
|
-
char version[
|
|
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];
|
|
118
|
-
char version[
|
|
119
|
-
uint8_t* doc_cbor;
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.21.1-next.20260927212749",
|
|
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.
|
|
89
|
+
"@mikrojs/quickjs": "0.21.1-next.20260927212749+7f87381",
|
|
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.
|
|
95
|
-
"@mikrojs/schema": "0.21.
|
|
95
|
+
"@mikrojs/registry": "0.21.1-next.20260927212749+7f87381",
|
|
96
|
+
"@mikrojs/schema": "0.21.1-next.20260927212749+7f87381",
|
|
96
97
|
"@swc/core": "^1.16.2",
|
|
97
98
|
"@types/node": "^24.12.2",
|
|
98
99
|
"esbuild": "^0.28.2",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runtime/kv/shared.ts
CHANGED
|
@@ -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/*', '
|
|
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
|
|
package/scripts/modules-list.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
}
|
package/src/gpio_claim.cpp
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/mik_app_config.cpp
CHANGED
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
static const char* TAG = "mik_app_config";
|
|
14
14
|
|
|
15
|
+
/* memReserved when the config names none. The firmware lowers it for a build
|
|
16
|
+
* without radios, which has no WiFi or TLS demand to keep heap for. */
|
|
17
|
+
#ifndef MIK_DEFAULT_MEM_RESERVED
|
|
18
|
+
#define MIK_DEFAULT_MEM_RESERVED (64 * 1024)
|
|
19
|
+
#endif
|
|
20
|
+
|
|
15
21
|
void MIK_DefaultConfig(MIKConfig* config) {
|
|
16
22
|
config->panic_restart_delay_ms = 1000;
|
|
17
23
|
config->panic_mode = MIK_PANIC_RESTART;
|
|
18
24
|
config->panic_sleep_duration_ms = 0;
|
|
19
25
|
config->stack_size = 0;
|
|
20
|
-
config->mem_reserved =
|
|
26
|
+
config->mem_reserved = MIK_DEFAULT_MEM_RESERVED;
|
|
21
27
|
config->fs_read_max = 0; /* 0 = runtime default (65536) */
|
|
22
28
|
config->entry_point[0] = '\0';
|
|
23
29
|
config->wifi_country[0] = '\0';
|
package/src/mik_ota_config.cpp
CHANGED
|
@@ -231,7 +231,7 @@ JSValue MIKOtaConfigReader::LoadDoc(JSContext* ctx, bool* out_failed) {
|
|
|
231
231
|
return JS_UNDEFINED;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
char version[
|
|
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[
|
|
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
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
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
|
-
|
|
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
|
|
1003
|
-
|
|
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
|
-
*
|
|
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,
|
|
1030
|
-
|
|
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 (
|
|
1034
|
-
|
|
1035
|
-
|
|
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,
|
|
1039
|
-
|
|
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,
|
|
1043
|
-
|
|
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
|
|
package/src/mik_result.cpp
CHANGED
|
@@ -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
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
{
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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"
|
|
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
|
|
702
|
-
*
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
|
734
|
-
//
|
|
735
|
-
|
|
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
|
-
/*
|
|
753
|
-
*
|
|
754
|
-
if (
|
|
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
|
}
|