From c65e0a14347f98a7bab72a0cd791156d05e0080f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 19 Jun 2026 13:23:10 +0200 Subject: [PATCH 1/3] Add `js_create_snapshot()` and `js_take_snapshot()` --- include/js.h | 164 ++++- src/js.cc | 896 ++++++++++++++++++++++-- test/CMakeLists.txt | 9 + test/boot-from-snapshot.c | 441 ++++++++++++ test/create-snapshot.c | 65 ++ test/external-references.c | 56 ++ test/fixtures/snapshots/.gitignore | 2 + test/run-from-snapshot-delegate.c | 89 +++ test/run-from-snapshot-dynamic-import.c | 165 +++++ test/run-from-snapshot-function.c | 88 +++ test/run-from-snapshot-import-meta.c | 78 +++ test/run-from-snapshot-module.c | 82 +++ test/run-from-snapshot-wrap.c | 75 ++ test/run-from-snapshot.c | 47 ++ test/snapshot.h | 302 ++++++++ 15 files changed, 2508 insertions(+), 51 deletions(-) create mode 100644 test/boot-from-snapshot.c create mode 100644 test/create-snapshot.c create mode 100644 test/external-references.c create mode 100644 test/fixtures/snapshots/.gitignore create mode 100644 test/run-from-snapshot-delegate.c create mode 100644 test/run-from-snapshot-dynamic-import.c create mode 100644 test/run-from-snapshot-function.c create mode 100644 test/run-from-snapshot-import-meta.c create mode 100644 test/run-from-snapshot-module.c create mode 100644 test/run-from-snapshot-wrap.c create mode 100644 test/run-from-snapshot.c create mode 100644 test/snapshot.h diff --git a/include/js.h b/include/js.h index dd7b94b..501c56b 100644 --- a/include/js.h +++ b/include/js.h @@ -17,6 +17,7 @@ typedef struct js_platform_options_s js_platform_options_t; typedef struct js_platform_limits_s js_platform_limits_t; typedef struct js_env_s js_env_t; typedef struct js_env_options_s js_env_options_t; +typedef struct js_snapshot_options_s js_snapshot_options_t; typedef struct js_handle_scope_s js_handle_scope_t; typedef struct js_escapable_handle_scope_s js_escapable_handle_scope_t; typedef struct js_context_s js_context_t; @@ -40,6 +41,10 @@ typedef struct js_error_location_s js_error_location_t; typedef struct js_inspector_s js_inspector_t; typedef struct js_garbage_collection_tracking_s js_garbage_collection_tracking_t; typedef struct js_garbage_collection_tracking_options_s js_garbage_collection_tracking_options_t; +typedef struct js_external_references_s js_external_references_t; +typedef struct js_rebind_handlers_s js_rebind_handlers_t; +typedef struct js_binding_payload_s js_binding_payload_t; +typedef struct js_rebind_info_s js_rebind_info_t; enum { /** @@ -179,6 +184,7 @@ typedef void (*js_deferred_teardown_cb)(js_deferred_teardown_t *, void *data); typedef void (*js_inspector_message_cb)(js_env_t *, js_inspector_t *, const char *message, size_t len, void *data); typedef bool (*js_inspector_paused_cb)(js_env_t *, js_inspector_t *, void *data); typedef void (*js_garbage_collection_cb)(js_garbage_collection_type_t, void *data); +typedef bool (*js_rebind_cb)(js_env_t *, const js_rebind_info_t *info, void *data, js_binding_payload_t *result); /** @version 1 */ struct js_platform_options_s { @@ -265,7 +271,7 @@ struct js_platform_limits_s { size_t string_length; }; -/** @version 0 */ +/** @version 1 */ struct js_env_options_s { int version; @@ -276,6 +282,74 @@ struct js_env_options_s { * @since 0 */ size_t memory_limit; + + /** + * A startup snapshot to boot the environment from instead of building its + * default context from scratch. When set, the bytes must remain valid for the + * lifetime of the environment. + * + * @since 1 + */ + const void *snapshot; + + /** + * The length of `snapshot` in bytes. + * + * @since 1 + */ + size_t snapshot_len; + + /** + * The external references reachable from the snapshot. Must be the same array, + * byte-for-byte, that was used to produce the snapshot, and must remain valid + * for the lifetime of the environment. + * + * @since 1 + */ + const js_external_references_t *external_references; + + /** + * Handlers consulted while booting from a snapshot to re-supply the + * per-instance `data` of reconstructed native bindings. May be NULL, in which + * case reconstructed bindings get no `data`. + * + * @since 1 + */ + const js_rebind_handlers_t *rebind_handlers; +}; + +typedef enum { + /** + * Clear compiled function code from the blob. Smaller, but functions are + * recompiled lazily on first call. + */ + js_snapshot_clear_function_code = 0, + + /** + * Keep compiled function code in the blob. Larger and locked to the exact + * engine build, but faster first execution. + */ + js_snapshot_keep_function_code = 1, +} js_snapshot_function_code_handling_t; + +/** @version 0 */ +struct js_snapshot_options_s { + int version; + + /** + * How to treat compiled function code when serializing. + * + * @since 0 + */ + js_snapshot_function_code_handling_t function_code_handling; + + /** + * The external references reachable from the snapshot. Must be the same array + * later passed to `js_create_env()` when booting from the resulting blob. + * + * @since 0 + */ + const js_external_references_t *external_references; }; /** @version 0 */ @@ -332,6 +406,48 @@ struct js_delegate_callbacks_s { js_delegate_own_keys_cb own_keys; }; +/** @version 0 */ +struct js_binding_payload_s { + /** @since 0 */ + void *data; + + /** @since 0 */ + void *finalize_hint; +}; + +typedef enum { + js_binding_function = 0, + js_binding_finalizer = 1, + js_binding_delegate = 2, +} js_binding_type_t; + +/** @version 0 */ +struct js_rebind_info_s { + int version; + + /** @since 0 */ + js_binding_type_t type; + + union { + /** @since 0 */ + struct { + js_function_cb cb; + } function; + + /** @since 0 */ + struct { + js_finalize_cb cb; + js_value_t *holder; + } finalizer; + + /** @since 0 */ + struct { + js_delegate_callbacks_t callbacks; + js_value_t *holder; + } delegate; + }; +}; + /** @version 0 */ struct js_type_tag_s { /** @since 0 */ @@ -447,12 +563,50 @@ js_get_platform_limits(js_platform_t *platform, js_platform_limits_t *result); int js_get_platform_loop(js_platform_t *platform, uv_loop_t **result); +int +js_create_external_references(js_external_references_t **result); + +int +js_add_external_reference(js_external_references_t *references, const void *address); + +int +js_get_external_references(js_external_references_t *references, const intptr_t **data, size_t *len); + +int +js_destroy_external_references(js_external_references_t *references); + +int +js_create_rebind_handlers(js_rebind_handlers_t **result); + +int +js_add_rebind_handler(js_rebind_handlers_t *handlers, js_rebind_cb cb, void *data); + +int +js_destroy_rebind_handlers(js_rebind_handlers_t *handlers); + int js_create_env(uv_loop_t *loop, js_platform_t *platform, const js_env_options_t *options, js_env_t **result); int js_destroy_env(js_env_t *env); +/** + * Create an environment set up for serialization. The returned environment is + * warmed up with the regular run/compile APIs, then serialized and consumed by + * `js_take_snapshot()`. + */ +int +js_create_snapshot(uv_loop_t *loop, js_platform_t *platform, const js_snapshot_options_t *options, js_env_t **result); + +/** + * Serialize a warmed-up snapshot environment into a blob and tear it down. The + * environment is consumed and must not be passed to `js_destroy_env()`. On + * success `*data` is a newly allocated buffer owned by the caller and freed with + * `free()`. + */ +int +js_take_snapshot(js_env_t *env, void **data, size_t *len); + /** * Add a callback for uncaught exceptions. By default, uncaught exceptions are * swallowed and do not affect JavaScript execution. @@ -596,6 +750,14 @@ js_get_module_name(js_env_t *env, js_module_t *module, const char **result); int js_get_module_id(js_env_t *env, js_module_t *module, js_value_t **result); +/** + * Look up a module reconstructed from a snapshot by its `id` and return its + * handle. Only fully evaluated modules survive a snapshot; their callbacks are + * never invoked again after restore, so none need to be re-supplied. + */ +int +js_get_module_by_id(js_env_t *env, js_value_t *id, js_module_t **result); + /** * Get the identifier shared by all compilation units that do not carry one of * their own, such as scripts run with `js_run_script()`. This is the `id` diff --git a/src/js.cc b/src/js.cc index dd64340..70ba4c6 100644 --- a/src/js.cc +++ b/src/js.cc @@ -35,6 +35,12 @@ using namespace v8_inspector; typedef struct js_callback_s js_callback_t; typedef struct js_typed_callback_s js_typed_callback_t; typedef struct js_finalizer_s js_finalizer_t; +template +struct js_binding_callbacks_s; +typedef struct js_binding_callbacks_s js_function_callbacks_t; +typedef struct js_binding_callbacks_s js_finalizer_callbacks_t; +typedef struct js_binding_state_s js_binding_state_t; +typedef struct js_snapshot_state_s js_snapshot_state_t; typedef struct js_delegate_s js_delegate_t; typedef struct js_external_string_utf16le_s js_external_string_utf16le_t; typedef struct js_external_string_latin1_s js_external_string_latin1_t; @@ -103,10 +109,15 @@ js_from_local(Local local) { namespace { -static const ExternalPointerTypeTag js_callback_info_type_tag = 1; -static const ExternalPointerTypeTag js_delegate_type_tag = 2; -static const ExternalPointerTypeTag js_finalizer_type_tag = 3; -static const ExternalPointerTypeTag js_external_type_tag = 4; +static const ExternalPointerTypeTag js_external_type_tag = 1; + +static const EmbedderDataTypeTag js_delegate_type_tag = 1; + +static StartupData +js_serialize_internal_field(Local holder, int index, void *data); + +static void +js_deserialize_internal_field(Local holder, int index, StartupData payload, void *data); } // namespace @@ -1305,6 +1316,59 @@ struct js_platform_s : public Platform { } }; +template +struct js_binding_callbacks_s { + std::vector entries; + std::vector free; + + inline uint32_t + add(T *entry) { + if (!free.empty()) { + auto index = free.back(); + + free.pop_back(); + + entries[index] = entry; + + return index; + } + + entries.push_back(entry); + + return uint32_t(entries.size() - 1); + } + + inline void + remove(uint32_t index, T *entry) { + if (index >= entries.size() || entries[index] != entry) return; + + entries[index] = nullptr; + + free.push_back(index); + } +}; + +struct js_binding_state_s { + js_function_callbacks_t functions; + + js_finalizer_callbacks_t finalizers; + + void + destroy_function_callbacks(); +}; + +struct js_snapshot_state_s { + SnapshotCreator *creator; + SnapshotCreator::FunctionCodeHandling function_code_handling; + + std::vector external_references; + + js_snapshot_state_s(std::vector references) + : creator(nullptr), + function_code_handling(SnapshotCreator::FunctionCodeHandling::kClear), + external_references(std::move(references)) {} +}; + struct js_env_s { uv_loop_t *loop; uv_prepare_t prepare; @@ -1354,7 +1418,11 @@ struct js_env_s { void *dynamic_import_data; } callbacks; - js_env_s(uv_loop_t *loop, js_platform_t *platform, Isolate *isolate) + js_binding_state_t bindings; + + js_snapshot_state_t snapshot; + + js_env_s(uv_loop_t *loop, js_platform_t *platform, Isolate *isolate, std::vector references, const js_rebind_handlers_t *rebind_handlers) : loop(loop), prepare(), check(), @@ -1375,7 +1443,9 @@ struct js_env_s { unhandled_promises(), teardown_queue(), inspector(), - callbacks() { + callbacks(), + bindings(), + snapshot(std::move(references)) { int err; std::unique_lock guard(platform->lock); @@ -1420,7 +1490,20 @@ struct js_env_s { auto scope = HandleScope(isolate); - context.Reset(isolate, Context::New(isolate)); + // Pass the delegate deserialize hook so a delegate's per-instance pointer is + // reconstructed from its internal field when booting from a snapshot. It is + // never invoked for a freshly created context. + context.Reset( + isolate, + Context::New( + isolate, + nullptr, + MaybeLocal(), + MaybeLocal(), + DeserializeInternalFieldsCallback(js_deserialize_internal_field, const_cast(rebind_handlers)) + ) + ); + context.Get(isolate)->Enter(); wrapper.Reset(isolate, Private::New(isolate)); @@ -1431,7 +1514,16 @@ struct js_env_s { ~js_env_s() { if (inspector) inspector.reset(); - { + bindings.destroy_function_callbacks(); + + // The finalizer table does not own its entries, so just drop the lookup + // mappings; any finalizer destructor firing during disposal then sees an + // empty table and skips its slot removal. + bindings.finalizers.entries.clear(); + bindings.finalizers.free.clear(); + + if (snapshot.creator) delete snapshot.creator; + else { auto scope = HandleScope(isolate); wrapper.Reset(); @@ -1442,9 +1534,10 @@ struct js_env_s { context.Get(isolate)->Exit(); context.Reset(); + + isolate->Exit(); } - isolate->Exit(); isolate->Dispose(); std::unique_lock guard(platform->lock); @@ -2090,36 +2183,51 @@ struct js_deferred_s { }; struct js_callback_s { - Global external; + Global function; + uint32_t index; js_env_t *env; js_function_cb cb; void *data; js_callback_s(js_env_t *env, js_function_cb cb, void *data) - : external(env->isolate, External::New(env->isolate, this, js_callback_info_type_tag)), + : function(), + index(env->bindings.functions.add(this)), env(env), cb(cb), - data(data) { - external.SetWeak(this, on_finalize, WeakCallbackType::kParameter); - } + data(data) {} + + js_callback_s(js_env_t *env, js_function_cb cb, void *data, uint32_t index) + : function(), + index(index), + env(env), + cb(cb), + data(data) {} js_callback_s(const js_callback_s &) = delete; - virtual ~js_callback_s() = default; + virtual ~js_callback_s() { + env->bindings.functions.remove(index, this); + } js_callback_s & operator=(const js_callback_s &) = delete; MaybeLocal to_function(Isolate *isolate, Local context) { - return Function::New( + auto function = Function::New( context, on_call, - external.Get(isolate), + Integer::NewFromUnsigned(isolate, index), 0, ConstructorBehavior::kAllow, SideEffectType::kHasSideEffect ); + + Local local; + + if (function.ToLocal(&local)) hold_weakly(isolate, local); + + return function; } Local @@ -2127,7 +2235,7 @@ struct js_callback_s { return FunctionTemplate::New( isolate, on_call, - external.Get(isolate), + Integer::NewFromUnsigned(isolate, index), signature, 0, ConstructorBehavior::kAllow, @@ -2135,12 +2243,18 @@ struct js_callback_s { ); } -protected: + void + hold_weakly(Isolate *isolate, Local function) { + this->function.Reset(isolate, function); + + this->function.SetWeak(static_cast(this), on_finalize, WeakCallbackType::kParameter); + } + static void on_call(const FunctionCallbackInfo &info) { - auto callback = reinterpret_cast(info.Data().As()->Value(js_callback_info_type_tag)); + auto env = js_env_t::from(info.GetIsolate()); - auto env = callback->env; + auto callback = env->bindings.functions.entries[size_t(info.Data().As()->Value())]; auto result = callback->cb(env, reinterpret_cast(const_cast *>(&info))); @@ -2157,6 +2271,7 @@ struct js_callback_s { } } +protected: static void on_finalize(const WeakCallbackInfo &info) { auto callback = info.GetParameter(); @@ -2165,6 +2280,19 @@ struct js_callback_s { } }; +void +js_binding_state_s::destroy_function_callbacks() { + std::vector callbacks; + + std::swap(callbacks, functions.entries); + + functions.free.clear(); + + for (auto callback : callbacks) { + delete callback; + } +} + struct js_typed_callback_s : js_callback_t { CTypeInfo result; std::vector args; @@ -2190,7 +2318,7 @@ struct js_typed_callback_s : js_callback_t { return FunctionTemplate::New( isolate, on_call, - external.Get(isolate), + Integer::NewFromUnsigned(isolate, index), signature, 0, ConstructorBehavior::kThrow, @@ -2201,22 +2329,30 @@ struct js_typed_callback_s : js_callback_t { }; struct js_finalizer_s { + static constexpr uint32_t unregistered = UINT32_MAX; + Global value; + uint32_t index; js_env_t *env; void *data; js_finalize_cb finalize_cb; void *finalize_hint; + bool is_delegate; js_finalizer_s(js_env_t *env, void *data, js_finalize_cb finalize_cb, void *finalize_hint) : value(), + index(unregistered), env(env), data(data), finalize_cb(finalize_cb), - finalize_hint(finalize_hint) {} + finalize_hint(finalize_hint), + is_delegate(false) {} js_finalizer_s(const js_finalizer_s &) = delete; - virtual ~js_finalizer_s() = default; + virtual ~js_finalizer_s() { + env->bindings.finalizers.remove(index, this); + } js_finalizer_s & operator=(const js_finalizer_s &) = delete; @@ -2262,7 +2398,9 @@ struct js_delegate_s : js_finalizer_t { js_delegate_s(js_env_t *env, js_delegate_callbacks_t callbacks, void *data, js_finalize_cb finalize_cb, void *finalize_hint) : js_finalizer_t(env, data, finalize_cb, finalize_hint), - callbacks(std::move(callbacks)) {} + callbacks(std::move(callbacks)) { + is_delegate = true; + } js_delegate_s(const js_delegate_s &) = delete; @@ -2271,38 +2409,42 @@ struct js_delegate_s : js_finalizer_t { Local to_object_template(Isolate *isolate) { - auto external = External::New(isolate, this, js_delegate_type_tag); - auto tpl = ObjectTemplate::New(isolate); + tpl->SetInternalFieldCount(1); + tpl->SetHandler(NamedPropertyHandlerConfiguration( on_get, on_set, nullptr, on_delete, - on_enumerate, - external + on_enumerate )); tpl->SetHandler(IndexedPropertyHandlerConfiguration( on_get_indexed, on_set_indexed, nullptr, - on_delete_indexed, - nullptr, - external + on_delete_indexed )); return tpl; } private: + template + static js_delegate_t * + from(const PropertyCallbackInfo &info) { + return reinterpret_cast(info.Holder()->GetAlignedPointerFromInternalField(0, js_delegate_type_tag)); + } + +public: template static Intercepted on_get(Local property, const PropertyCallbackInfo &info) { auto env = js_env_t::from(info.GetIsolate()); - auto delegate = static_cast(info.Data().As()->Value(js_delegate_type_tag)); + auto delegate = from(info); if (delegate->callbacks.has) { auto exists = delegate->callbacks.has(env, js_from_local(property), delegate->data); @@ -2341,7 +2483,7 @@ struct js_delegate_s : js_finalizer_t { on_set(Local property, Local value, const PropertyCallbackInfo &info) { auto env = js_env_t::from(info.GetIsolate()); - auto delegate = static_cast(info.Data().As()->Value(js_delegate_type_tag)); + auto delegate = from(info); if (delegate->callbacks.set) { auto result = delegate->callbacks.set(env, js_from_local(property), js_from_local(value), delegate->data); @@ -2372,7 +2514,7 @@ struct js_delegate_s : js_finalizer_t { on_delete(Local property, const PropertyCallbackInfo &info) { auto env = js_env_t::from(info.GetIsolate()); - auto delegate = static_cast(info.Data().As()->Value(js_delegate_type_tag)); + auto delegate = from(info); if (delegate->callbacks.delete_property) { auto result = delegate->callbacks.delete_property(env, js_from_local(property), delegate->data); @@ -2402,7 +2544,7 @@ struct js_delegate_s : js_finalizer_t { on_enumerate(const PropertyCallbackInfo &info) { auto env = js_env_t::from(info.GetIsolate()); - auto delegate = static_cast(info.Data().As()->Value(js_delegate_type_tag)); + auto delegate = from(info); if (delegate->callbacks.own_keys) { auto result = delegate->callbacks.own_keys(env, delegate->data); @@ -3253,6 +3395,367 @@ js_option(const js_env_options_t *options, int min_version, T fallback = T(js_en } // namespace +struct js_external_references_s { + std::vector addresses; +}; + +extern "C" int +js_create_external_references(js_external_references_t **result) { + *result = new js_external_references_t(); + + return 0; +} + +extern "C" int +js_add_external_reference(js_external_references_t *references, const void *address) { + auto &addresses = references->addresses; + + // Drop any terminator a prior `js_get_external_references` appended so that + // adding remains valid regardless of call order. A real reference is never + // null, so a trailing zero can only be the terminator. + if (!addresses.empty() && addresses.back() == 0) addresses.pop_back(); + + addresses.push_back(reinterpret_cast(address)); + + return 0; +} + +extern "C" int +js_get_external_references(js_external_references_t *references, const intptr_t **data, size_t *len) { + auto &addresses = references->addresses; + + // V8 expects a null-terminated array. Maintain the terminator lazily so the + // returned pointer is stable and safe to hand to `Isolate::CreateParams`. + if (addresses.empty() || addresses.back() != 0) addresses.push_back(0); + + if (data) *data = addresses.data(); + if (len) *len = addresses.size() - 1; // Excluding the terminator. + + return 0; +} + +extern "C" int +js_destroy_external_references(js_external_references_t *references) { + delete references; + + return 0; +} + +struct js_rebind_handlers_s { + struct entry { + js_rebind_cb cb; + void *data; + }; + + std::vector entries; +}; + +extern "C" int +js_create_rebind_handlers(js_rebind_handlers_t **result) { + *result = new js_rebind_handlers_t(); + + return 0; +} + +extern "C" int +js_add_rebind_handler(js_rebind_handlers_t *handlers, js_rebind_cb cb, void *data) { + handlers->entries.push_back({cb, data}); + + return 0; +} + +extern "C" int +js_destroy_rebind_handlers(js_rebind_handlers_t *handlers) { + delete handlers; + + return 0; +} + +namespace { + +// Snapshot data slot layout, shared by `js_take_snapshot` (which `AddData`s in +// this order) and `js_rebind_from_snapshot` (which reads it back). The metadata +// array and the wrapper symbol occupy fixed slots; each serialized module record +// follows in iteration order, so module `i` lands at `js_snapshot_data_modules +// + i`. +enum { + js_snapshot_data_metadata = 0, + js_snapshot_data_wrapper = 1, + js_snapshot_data_modules = 2, +}; + +// Metadata array layout: the manifests and id list `js_rebind_from_snapshot` +// replays, indexed within the metadata array stored at +// `js_snapshot_data_metadata`. +enum { + js_snapshot_metadata_functions = 0, + js_snapshot_metadata_wraps = 1, + js_snapshot_metadata_module_ids = 2, +}; + +// Asks each handler, in order, to claim a reconstructed binding - described by +// the populated `info` (its type tag, the resolved callbacks, and any holder) - +// and supply its `data` / `finalize_hint`. Returns an empty payload when none +// claim it. +static js_binding_payload_t +js_rebind(const js_rebind_handlers_t *handlers, js_env_t *env, const js_rebind_info_t &info) { + if (handlers != nullptr) { + for (const auto &entry : handlers->entries) { + js_binding_payload_t result = {nullptr, nullptr}; + + if (entry.cb(env, &info, entry.data, &result)) { + return result; + } + } + } + + return {nullptr, nullptr}; +} + +// Finds a native address's position in the combined external-reference array, so +// it can be named in a snapshot manifest. Every snapshotted callback must be a +// registered reference. +static uint32_t +js_external_reference_index(js_env_t *env, const void *address) { + auto value = reinterpret_cast(address); + + size_t index = 0; + while (index < env->snapshot.external_references.size() && env->snapshot.external_references[index] != value) { + index++; + } + + assert(index < env->snapshot.external_references.size()); + + return uint32_t(index); +} + +// Resolves an index in the combined external-reference array back to its +// address, the inverse of `js_external_reference_index`. A negative index +// denotes an absent callback and resolves to null. +static void * +js_external_reference(js_env_t *env, int64_t index) { + return index < 0 ? nullptr : reinterpret_cast(env->snapshot.external_references[size_t(index)]); +} + +// The serialized form of a delegate's internal field: its callbacks' positions +// in the combined external-reference array (-1 for a null callback). `data` and +// `finalize_hint` are not serializable and are re-supplied on load. +struct js_delegate_fields_t { + int32_t version; + int32_t get; + int32_t has; + int32_t set; + int32_t delete_property; + int32_t own_keys; + int32_t finalize_cb; +}; + +static StartupData +js_serialize_internal_field(Local holder, int index, void *data) { + auto env = js_env_t::from(Isolate::GetCurrent()); + + auto delegate = reinterpret_cast(holder->GetAlignedPointerFromInternalField(index, js_delegate_type_tag)); + + auto reference = [&](const void *address) { + return address ? int32_t(js_external_reference_index(env, address)) : -1; + }; + + auto fields = new js_delegate_fields_t{ + delegate->callbacks.version, + reference(reinterpret_cast(delegate->callbacks.get)), + reference(reinterpret_cast(delegate->callbacks.has)), + reference(reinterpret_cast(delegate->callbacks.set)), + reference(reinterpret_cast(delegate->callbacks.delete_property)), + reference(reinterpret_cast(delegate->callbacks.own_keys)), + reference(reinterpret_cast(delegate->finalize_cb)), + }; + + return StartupData{reinterpret_cast(fields), int(sizeof(js_delegate_fields_t))}; +} + +static void +js_deserialize_internal_field(Local holder, int index, StartupData payload, void *data) { + if (payload.raw_size != int(sizeof(js_delegate_fields_t))) return; + + auto handlers = reinterpret_cast(data); + + auto env = js_env_t::from(Isolate::GetCurrent()); + + auto fields = reinterpret_cast(payload.data); + + auto resolve = [&](int32_t reference) -> void * { + return js_external_reference(env, reference); + }; + + js_delegate_callbacks_t callbacks = {}; + callbacks.version = fields->version; + callbacks.get = reinterpret_cast(resolve(fields->get)); + callbacks.has = reinterpret_cast(resolve(fields->has)); + callbacks.set = reinterpret_cast(resolve(fields->set)); + callbacks.delete_property = reinterpret_cast(resolve(fields->delete_property)); + callbacks.own_keys = reinterpret_cast(resolve(fields->own_keys)); + + auto finalize_cb = reinterpret_cast(resolve(fields->finalize_cb)); + + js_rebind_info_t info = {}; + info.version = 0; + info.type = js_binding_delegate; + info.delegate.callbacks = callbacks; + info.delegate.holder = js_from_local(holder); + + auto rebound = js_rebind(handlers, env, info); + + auto delegate = new js_delegate_t(env, callbacks, rebound.data, finalize_cb, rebound.finalize_hint); + + holder->SetAlignedPointerInInternalField(index, delegate, js_delegate_type_tag); + + delegate->attach_to(env->isolate, holder); +} + +// Builds the null-terminated external-reference array V8 needs: libjs's stable +// core addresses first, then the embedder's, in a fixed order shared by snapshot +// production and consumption. Snapshot manifest entries index into the result. +static void +js_build_external_references(std::vector &result, const js_external_references_t *references) { + result.clear(); + + result.push_back(reinterpret_cast(&js_callback_t::on_call)); + result.push_back(reinterpret_cast(&js_env_t::on_uncaught_exception)); + result.push_back(reinterpret_cast(&js_env_t::on_promise_reject)); + result.push_back(reinterpret_cast(&js_module_t::on_dynamic_import)); + result.push_back(reinterpret_cast(&js_module_t::on_import_meta)); + result.push_back(reinterpret_cast(&js_delegate_t::on_get)); + result.push_back(reinterpret_cast(&js_delegate_t::on_set)); + result.push_back(reinterpret_cast(&js_delegate_t::on_delete)); + result.push_back(reinterpret_cast(&js_delegate_t::on_enumerate)); + result.push_back(reinterpret_cast(&js_delegate_t::on_get_indexed)); + result.push_back(reinterpret_cast(&js_delegate_t::on_set_indexed)); + result.push_back(reinterpret_cast(&js_delegate_t::on_delete_indexed)); + + if (references) { + for (auto address : references->addresses) { + if (address != 0) result.push_back(address); // Skip a lazily-added terminator. + } + } + + result.push_back(0); +} + +// Rebuilds native binding state after deserializing a snapshot, from the data +// `js_take_snapshot` attaches: a `[function manifest, wrap manifest]` array at +// index 0 and the wrapper `Private` symbol at index 1. +// +// - function manifest: flat `[slot, cb-index]` pairs; rebuilds the function +// side table, with `data` from the rebind handlers. +// - wrapper symbol: adopted as `env->wrapper` so deserialized wrapped objects' +// private properties resolve. +// - wrap manifest: flat `[object, slot, cb-index]` triples; rebuilds the +// finalizer side table and re-arms each finalizer (`cb-index` < 0 means no +// finalize callback). +static void +js_rebind_from_snapshot(js_env_t *env, const js_rebind_handlers_t *handlers) { + auto isolate = env->isolate; + + auto scope = HandleScope(isolate); + + auto context = env->current_context(); + + Local value; + if (!context->GetDataFromSnapshotOnce(js_snapshot_data_metadata).ToLocal(&value)) return; + if (!value->IsArray()) return; + + auto metadata = value.As(); + + auto functions = metadata->Get(context, js_snapshot_metadata_functions).ToLocalChecked().As(); + auto wraps = metadata->Get(context, js_snapshot_metadata_wraps).ToLocalChecked().As(); + + // Adopt the serialized wrapper symbol so private properties on deserialized + // wrapped objects, keyed by it, resolve. + Local wrapper; + if (context->GetDataFromSnapshotOnce(js_snapshot_data_wrapper).ToLocal(&wrapper)) { + env->wrapper.Reset(isolate, wrapper); + } + + for (uint32_t i = 0; i + 1 < functions->Length(); i += 2) { + auto slot = uint32_t(functions->Get(context, i).ToLocalChecked().As()->Value()); + auto cb_index = functions->Get(context, i + 1).ToLocalChecked().As()->Value(); + + auto cb = reinterpret_cast(js_external_reference(env, cb_index)); + + js_rebind_info_t info = {}; + info.version = 0; + info.type = js_binding_function; + info.function.cb = cb; + + auto data = js_rebind(handlers, env, info).data; + + if (env->bindings.functions.entries.size() <= slot) { + env->bindings.functions.entries.resize(slot + 1, nullptr); + } + + env->bindings.functions.entries[slot] = new js_callback_t(env, cb, data, slot); + } + + for (uint32_t i = 0; i + 2 < wraps->Length(); i += 3) { + auto object = wraps->Get(context, i).ToLocalChecked().As(); + auto slot = uint32_t(wraps->Get(context, i + 1).ToLocalChecked().As()->Value()); + auto cb_index = wraps->Get(context, i + 2).ToLocalChecked().As()->Value(); + + auto finalize_cb = reinterpret_cast(js_external_reference(env, cb_index)); + + js_rebind_info_t info = {}; + info.version = 0; + info.type = js_binding_finalizer; + info.finalizer.cb = finalize_cb; + info.finalizer.holder = js_from_local(object); + + auto payload = js_rebind(handlers, env, info); + + auto finalizer = new js_finalizer_t(env, payload.data, finalize_cb, payload.finalize_hint); + + finalizer->index = slot; + + if (env->bindings.finalizers.entries.size() <= slot) { + env->bindings.finalizers.entries.resize(slot + 1, nullptr); + } + + env->bindings.finalizers.entries[slot] = finalizer; + + finalizer->attach_to(isolate, object); + } + + // Module records: rebuild a `js_module_t` for each serialized module, paired by + // index with its id symbol. Only fully evaluated modules are serialized, so + // their callbacks are never invoked again and are left empty; the embedder + // reconnects its handles to the records by id via `js_get_module_by_id`. + auto modules = metadata->Get(context, js_snapshot_metadata_module_ids).ToLocalChecked().As(); + + for (uint32_t i = 0; i < modules->Length(); i++) { + auto id = modules->Get(context, i).ToLocalChecked().As(); + + Local module; + if (!context->GetDataFromSnapshotOnce(js_snapshot_data_modules + i).ToLocal(&module)) continue; + + // Recover the name from the module's resource name (the same string passed at + // creation, for both source-text and synthetic modules). + std::string name; + + auto resource = module->GetResourceName(); + if (!resource.IsEmpty() && resource->IsString()) { + auto string = resource.As(); + auto length = string->Utf8LengthV2(isolate); + name.resize(length); + string->WriteUtf8V2(isolate, name.data(), length, String::WriteFlags::kReplaceInvalidUtf8); + } + + auto record = new js_module_t(isolate, module, id, std::move(name)); + + env->modules.emplace(module->GetIdentityHash(), record); + } +} + +} // namespace + extern "C" int js_create_env(uv_loop_t *loop, js_platform_t *platform, const js_env_options_t *options, js_env_t **result) { Isolate::CreateParams params; @@ -3276,6 +3779,33 @@ js_create_env(uv_loop_t *loop, js_platform_t *platform, const js_env_options_t * } } + auto user_references = js_option<&js_env_options_t::external_references, const js_external_references_t *>(options, 1); + + auto snapshot = js_option<&js_env_options_t::snapshot, const void *>(options, 1); + auto snapshot_len = js_option<&js_env_options_t::snapshot_len, size_t>(options, 1); + + // The combined core + embedder reference array. Built whenever a snapshot is + // consumed (the blob references libjs's core addresses) or the embedder + // supplies references. Moved onto the env below so it outlives the lazy + // deserialization that reads it. + std::vector external_references; + + if (snapshot || user_references) { + js_build_external_references(external_references, user_references); + params.external_references = external_references.data(); + } + + // Must outlive `Isolate::Initialize()` below; the snapshot bytes themselves + // must outlive the environment, as V8 deserializes from them lazily. + StartupData startup_data; + + if (snapshot) { + startup_data.data = static_cast(snapshot); + startup_data.raw_size = static_cast(snapshot_len); + + params.snapshot_blob = &startup_data; + } + auto isolate = Isolate::Allocate(); auto tasks = new js_task_runner_t(loop); @@ -3300,7 +3830,15 @@ js_create_env(uv_loop_t *loop, js_platform_t *platform, const js_env_options_t * isolate->SetHostInitializeImportMetaObjectCallback(js_module_t::on_import_meta); - auto env = new js_env_t(loop, platform, isolate); + auto rebind_handlers = js_option<&js_env_options_t::rebind_handlers, const js_rebind_handlers_t *>(options, 1); + + // The reference buffer is moved, not copied, so the pointer handed to V8 above + // stays valid for the lifetime of the env. The rebind handlers are needed + // during construction (the delegate deserialize hook fires inside + // `Context::New`) as well as for the function and wrap rebuild below. + auto env = new js_env_t(loop, platform, isolate, std::move(external_references), rebind_handlers); + + if (snapshot) js_rebind_from_snapshot(env, rebind_handlers); *result = env; @@ -3314,6 +3852,224 @@ js_destroy_env(js_env_t *env) { return 0; } +namespace { + +static const js_snapshot_options_t js_snapshot_default_options = { + .version = 0, +}; + +template +static inline T +js_option(const js_snapshot_options_t *options, int min_version, T fallback = T(js_snapshot_default_options.*P)) { + return T(options && options->version >= min_version ? options->*P : fallback); +} + +} // namespace + +extern "C" int +js_create_snapshot(uv_loop_t *loop, js_platform_t *platform, const js_snapshot_options_t *options, js_env_t **result) { + Isolate::CreateParams params; + + params.array_buffer_allocator_shared = std::make_shared(); + + auto user_references = js_option<&js_snapshot_options_t::external_references, const js_external_references_t *>(options, 0); + + // The producer always needs the combined core + embedder references: `on_call` + // and the isolate callbacks installed below are serialized by index. Moved onto + // the env after construction so it outlives `CreateBlob`. + std::vector external_references; + js_build_external_references(external_references, user_references); + params.external_references = external_references.data(); + + // Allocate the isolate ourselves and register its task runner *before* it is + // initialised, exactly as `js_create_env` does: the snapshot creator + // initialises and enters the isolate, which makes V8 call + // `GetForegroundTaskRunner` (an `operator[]` insert), and a later `emplace` + // would not overwrite that null entry. We therefore use the non-owning + // creator and dispose the isolate ourselves at teardown. + auto isolate = Isolate::Allocate(); + + auto tasks = new js_task_runner_t(loop); + + std::unique_lock guard(platform->lock); + + platform->foreground.emplace(isolate, std::move(tasks)); + + guard.unlock(); + + auto creator = new SnapshotCreator(isolate, params); + + isolate->SetMicrotasksPolicy(MicrotasksPolicy::kExplicit); + + isolate->AddMessageListener(js_env_t::on_uncaught_exception); + + isolate->SetPromiseRejectCallback(js_env_t::on_promise_reject); + + isolate->SetHostImportModuleDynamicallyCallback(js_module_t::on_dynamic_import); + + isolate->SetHostInitializeImportMetaObjectCallback(js_module_t::on_import_meta); + + auto env = new js_env_t(loop, platform, isolate, std::move(external_references), nullptr); + + env->snapshot.creator = creator; + + env->snapshot.function_code_handling = + js_option<&js_snapshot_options_t::function_code_handling, js_snapshot_function_code_handling_t>(options, 0) == js_snapshot_keep_function_code + ? SnapshotCreator::FunctionCodeHandling::kKeep + : SnapshotCreator::FunctionCodeHandling::kClear; + + *result = env; + + return 0; +} + +extern "C" int +js_take_snapshot(js_env_t *env, void **data, size_t *len) { + auto creator = env->snapshot.creator; + + auto isolate = env->isolate; + + { + auto scope = HandleScope(isolate); + + auto context = env->context.Get(isolate); + + // Snapshot metadata, built while the context is still entered and replayed by + // `js_rebind_from_snapshot` on load: a 3-element array + // `[function manifest, wrap manifest, module ids]`, plus the wrapper symbol + // and each module record attached as their own data slots. + + // Function manifest: `[slot, cb-index]` pairs, `cb-index` indexing the + // combined external-reference array. + auto functions = Array::New(isolate); + + uint32_t fn = 0; + + for (uint32_t slot = 0; slot < env->bindings.functions.entries.size(); slot++) { + auto callback = env->bindings.functions.entries[slot]; + + if (callback == nullptr) continue; + + auto cb_index = js_external_reference_index(env, reinterpret_cast(callback->cb)); + + functions->Set(context, fn++, Integer::NewFromUnsigned(isolate, slot)).Check(); + functions->Set(context, fn++, Integer::NewFromUnsigned(isolate, cb_index)).Check(); + } + + // Wrap manifest: `[object, slot, cb-index]` triples, the wrapped object + // stored inline so the consumer gets a handle to re-arm its finalizer + // (`cb-index` -1 when there is no finalize callback). + auto wraps = Array::New(isolate); + + uint32_t wn = 0; + + for (uint32_t slot = 0; slot < env->bindings.finalizers.entries.size(); slot++) { + auto finalizer = env->bindings.finalizers.entries[slot]; + + if (finalizer == nullptr || finalizer->value.IsEmpty() || finalizer->is_delegate) continue; + + auto cb_index = finalizer->finalize_cb + ? int32_t(js_external_reference_index(env, reinterpret_cast(finalizer->finalize_cb))) + : -1; + + wraps->Set(context, wn++, finalizer->value.Get(isolate)).Check(); + wraps->Set(context, wn++, Integer::NewFromUnsigned(isolate, slot)).Check(); + wraps->Set(context, wn++, Integer::New(isolate, cb_index)).Check(); + } + + // Module ids: the id symbol of every module, in iteration order. Each + // module's compiled record is added below as snapshot data at index + // `js_snapshot_data_modules + i`, so the consumer pairs `ids[i]` with the + // module at the same offset. + auto modules = Array::New(isolate); + + std::vector> module_records; + + uint32_t mn = 0; + + for (const auto &entry : env->modules) { + auto module = entry.second; + + auto record = module->module.Get(isolate); + + // Only fully evaluated modules survive a snapshot: their callbacks are + // never invoked again, and other states (instantiating, mid-top-level-await, + // errored) carry no meaningful warmed-up result. + if (record->GetStatus() != Module::Status::kEvaluated) continue; + + modules->Set(context, mn++, module->id.Get(isolate)).Check(); + module_records.push_back(record); + } + + auto metadata = Array::New(isolate); + metadata->Set(context, js_snapshot_metadata_functions, functions).Check(); + metadata->Set(context, js_snapshot_metadata_wraps, wraps).Check(); + metadata->Set(context, js_snapshot_metadata_module_ids, modules).Check(); + + // The wrapper symbol is a `Private`, not a `Value`, so it rides its own data + // slot rather than the metadata array. + auto wrapper = env->wrapper.Get(isolate); + + context->Exit(); + + creator->SetDefaultContext(context, SerializeInternalFieldsCallback(js_serialize_internal_field)); + + // `AddData` assigns indices in call order; assert that order matches the + // shared slot layout the consumer reads back. + auto metadata_index = creator->AddData(context, metadata); + assert(metadata_index == size_t(js_snapshot_data_metadata)); + + auto wrapper_index = creator->AddData(context, wrapper); + assert(wrapper_index == size_t(js_snapshot_data_wrapper)); + + for (size_t i = 0; i < module_records.size(); i++) { + auto module_index = creator->AddData(context, module_records[i]); + assert(module_index == size_t(js_snapshot_data_modules) + i); + } + } + + // Release every Global into the isolate before serializing: `CreateBlob()` + // takes ownership of the default context and rejects any lingering global + // handles into the serialized heap. The per-env privates are re-created from + // scratch when the blob is booted, and the side table's weak handles on each + // function instance are no longer needed (the table is rebuilt on load). + env->wrapper.Reset(); + env->delegate.Reset(); + env->tag.Reset(); + env->exception.Reset(); + env->default_module_id.Reset(); + env->context.Reset(); + + for (auto callback : env->bindings.functions.entries) { + if (callback) callback->function.Reset(); + } + + for (auto finalizer : env->bindings.finalizers.entries) { + if (finalizer) finalizer->value.Reset(); + } + + for (auto &entry : env->modules) { + entry.second->module.Reset(); + entry.second->id.Reset(); + } + + // Must run outside any handle scope. + auto blob = creator->CreateBlob(env->snapshot.function_code_handling); + + *data = malloc(size_t(blob.raw_size)); + memcpy(*data, blob.data, size_t(blob.raw_size)); + *len = size_t(blob.raw_size); + + // `CreateBlob()` hands back a buffer allocated with `new[]`. + delete[] blob.data; + + // Consume the environment. The destructor deletes the creator, which disposes + // the isolate. + env->close_maybe(); + + return 0; +} + extern "C" int js_on_uncaught_exception(js_env_t *env, js_uncaught_exception_cb cb, void *data) { env->callbacks.uncaught_exception = cb; @@ -3645,6 +4401,30 @@ js_get_module_id(js_env_t *env, js_module_t *module, js_value_t **result) { return 0; } +extern "C" int +js_get_module_by_id(js_env_t *env, js_value_t *id, js_module_t **result) { + if (env->is_exception_pending()) return js_error(env); + + int err; + + auto symbol = js_to_local(id); + + for (const auto &entry : env->modules) { + auto module = entry.second; + + if (module->id.Get(env->isolate) == symbol) { + *result = module; + + return 0; + } + } + + err = js_throw_error(env, NULL, "Could not find module"); + assert(err == 0); + + return js_error(env); +} + extern "C" int js_get_default_module_id(js_env_t *env, js_value_t **result) { // Allow continuing even with a pending exception @@ -3982,11 +4762,13 @@ js_wrap(js_env_t *env, js_value_t *object, void *data, js_finalize_cb finalize_c auto finalizer = new js_finalizer_t(env, data, finalize_cb, finalize_hint); - auto external = External::New(env->isolate, finalizer, js_finalizer_type_tag); + finalizer->index = env->bindings.finalizers.add(finalizer); + + auto token = Integer::NewFromUnsigned(env->isolate, finalizer->index); auto success = env->try_catch( [&] { - return local->SetPrivate(context, key, external); + return local->SetPrivate(context, key, token); } ); @@ -4013,15 +4795,17 @@ js_unwrap(js_env_t *env, js_value_t *object, void **result) { auto local = js_to_local(object); - auto external = env->try_catch( + auto token = env->try_catch( [&] { return local->GetPrivate(context, key); } ); - if (external.IsEmpty()) return js_error(env); + if (token.IsEmpty()) return js_error(env); + + auto index = size_t(token.ToLocalChecked().As()->Value()); - auto finalizer = reinterpret_cast(external.ToLocalChecked().As()->Value(js_finalizer_type_tag)); + auto finalizer = env->bindings.finalizers.entries[index]; *result = finalizer->data; @@ -4038,17 +4822,19 @@ js_remove_wrap(js_env_t *env, js_value_t *object, void **result) { auto local = js_to_local(object); - auto external = env->try_catch( + auto token = env->try_catch( [&] { return local->GetPrivate(context, key); } ); - if (external.IsEmpty()) return js_error(env); + if (token.IsEmpty()) return js_error(env); local->DeletePrivate(context, key).Check(); - auto finalizer = reinterpret_cast(external.ToLocalChecked().As()->Value(js_finalizer_type_tag)); + auto index = size_t(token.ToLocalChecked().As()->Value()); + + auto finalizer = env->bindings.finalizers.entries[index]; finalizer->detach(); @@ -4067,15 +4853,21 @@ js_create_delegate(js_env_t *env, const js_delegate_callbacks_t *callbacks, void auto delegate = new js_delegate_t(env, *callbacks, data, finalize_cb, finalize_hint); + delegate->index = env->bindings.finalizers.add(delegate); + auto tpl = delegate->to_object_template(env->isolate); auto object = tpl->NewInstance(context); if (object.IsEmpty()) return js_error(env); - delegate->attach_to(env->isolate, object.ToLocalChecked()); + auto local = object.ToLocalChecked(); - *result = js_from_local(object.ToLocalChecked()); + local->SetAlignedPointerInInternalField(0, delegate, js_delegate_type_tag); + + delegate->attach_to(env->isolate, local); + + *result = js_from_local(local); return 0; } @@ -4698,6 +5490,8 @@ js_create_typed_function(js_env_t *env, const char *name, size_t len, js_functio local->SetName(string.ToLocalChecked()); } + callback->hold_weakly(env->isolate, local); + *result = js_from_local(local); return 0; @@ -6636,7 +7430,7 @@ js_get_callback_info(js_env_t *env, const js_callback_info_t *info, size_t *argc if (receiver) *receiver = js_from_local(args->This()); if (data) { - *data = reinterpret_cast(args->Data().As()->Value(js_callback_info_type_tag))->data; + *data = env->bindings.functions.entries[size_t(args->Data().As()->Value())]->data; } return 0; @@ -6648,10 +7442,12 @@ js_get_typed_callback_info(const js_typed_callback_info_t *info, js_env_t **env, auto args = reinterpret_cast(info); - if (env) *env = js_env_t::from(args->isolate); + auto callback_env = js_env_t::from(args->isolate); + + if (env) *env = callback_env; if (data) { - *data = reinterpret_cast(args->data.As()->Value(js_callback_info_type_tag))->data; + *data = callback_env->bindings.functions.entries[size_t(args->data.As()->Value())]->data; } return 0; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7244855..343db7d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -82,6 +82,7 @@ list(APPEND tests create-reference-undefined create-sharedarraybuffer create-sharedarraybuffer-with-backing-store + create-snapshot create-string-latin1 create-string-utf8 create-string-utf8-with-null @@ -127,6 +128,7 @@ list(APPEND tests dynamic-import-with-referrer-function dynamic-import-without-handler escapable-handle-scope + external-references fatal-exception garbage-collection-tracking get-array-elements @@ -209,6 +211,13 @@ list(APPEND tests promise-rejection-unhandled-reentrant promise-rejection-unhandled-reentrant-deferred reference-unref + run-from-snapshot + run-from-snapshot-delegate + run-from-snapshot-dynamic-import + run-from-snapshot-function + run-from-snapshot-import-meta + run-from-snapshot-module + run-from-snapshot-wrap run-module run-module-async run-module-cyclic-import diff --git a/test/boot-from-snapshot.c b/test/boot-from-snapshot.c new file mode 100644 index 0000000..4a23a8c --- /dev/null +++ b/test/boot-from-snapshot.c @@ -0,0 +1,441 @@ +#include +#include +#include +#include +#include +#include + +#include "../include/js.h" + +// Verifies that a native function survives an isolate snapshot: the producer +// binds a native `answer()` onto the global and serializes a blob; a separate +// process boots from the blob and calls `globalThis.answer()`, expecting 42. +// +// Producer and consumer run in different processes (the test re-spawns itself) +// because this build enables V8's shared read-only heap, which makes in-process +// consumption of a freshly produced blob trip a debug-only checksum CHECK. + +static js_value_t * +answer(js_env_t *env, js_callback_info_t *info) { + int e; + + // Return the callback's `data`, so the test only passes if `data` is rebound + // after restore (it would read NULL, and return 0, without a rebind handler). + void *data; + e = js_get_callback_info(env, info, NULL, NULL, NULL, &data); + assert(e == 0); + + js_value_t *result; + e = js_create_int32(env, (int32_t) (intptr_t) data, &result); + assert(e == 0); + + return result; +} + +static void +on_wrap_finalize(js_env_t *env, void *data, void *finalize_hint) {} + +// A delegate getter that returns its data for any property. +static js_value_t * +delegate_get(js_env_t *env, js_value_t *property, void *data) { + int e; + + js_value_t *result; + e = js_create_int32(env, (int32_t) (intptr_t) data, &result); + assert(e == 0); + + return result; +} + +static bool +rebind(js_env_t *env, js_binding_type_t type, const void *cb, js_value_t *holder, void *context, js_binding_payload_t *result) { + if (type == js_binding_function && cb == (const void *) answer) { + result->data = (void *) (intptr_t) 42; + + return true; + } + + if (type == js_binding_finalizer && cb == (const void *) on_wrap_finalize) { + result->data = (void *) (intptr_t) 99; + + return true; + } + + if (type == js_binding_delegate && cb == (const void *) delegate_get) { + result->data = (void *) (intptr_t) 123; + + return true; + } + + return false; +} + +static void +on_meta(js_env_t *env, js_module_t *module, js_value_t *meta, void *data) { + int e; + + js_value_t *value; + e = js_create_int32(env, 7, &value); + assert(e == 0); + + e = js_set_named_property(env, meta, "answer", value); + assert(e == 0); +} + +static js_external_references_t * +references() { + int e; + + js_external_references_t *references; + e = js_create_external_references(&references); + assert(e == 0); + + e = js_add_external_reference(references, (const void *) answer); + assert(e == 0); + + e = js_add_external_reference(references, (const void *) on_wrap_finalize); + assert(e == 0); + + e = js_add_external_reference(references, (const void *) delegate_get); + assert(e == 0); + + return references; +} + +static int exit_status = -1; + +static void +on_exit(uv_process_t *process, int64_t status, int signal) { + exit_status = (int) status; + + uv_close((uv_handle_t *) process, NULL); +} + +static void +produce(uv_loop_t *loop, js_platform_t *platform) { + int e; + + js_snapshot_options_t options = { + .version = 0, + .external_references = references(), + }; + + js_env_t *env; + e = js_create_snapshot(loop, platform, &options, &env); + assert(e == 0); + + js_handle_scope_t *scope; + e = js_open_handle_scope(env, &scope); + assert(e == 0); + + js_value_t *global; + e = js_get_global(env, &global); + assert(e == 0); + + js_value_t *fn; + e = js_create_function(env, "answer", -1, answer, (void *) (intptr_t) 42, &fn); + assert(e == 0); + + e = js_set_named_property(env, global, "answer", fn); + assert(e == 0); + + // Wrap an object reachable from the global, with non-NULL data and a finalizer. + // After restore its finalizer side-table slot is rebuilt and the data rebound. + js_value_t *wrapped; + e = js_create_object(env, &wrapped); + assert(e == 0); + + e = js_wrap(env, wrapped, (void *) (intptr_t) 99, on_wrap_finalize, NULL, NULL); + assert(e == 0); + + e = js_set_named_property(env, global, "wrapped", wrapped); + assert(e == 0); + + // Create a delegate reachable from the global. After restore its internal + // field is reconstructed and its data rebound, so property access routes + // through the rebound getter. + js_delegate_callbacks_t callbacks = { + .version = 0, + .get = delegate_get, + }; + + js_value_t *delegate; + e = js_create_delegate(env, &callbacks, (void *) (intptr_t) 123, NULL, NULL, &delegate); + assert(e == 0); + + e = js_set_named_property(env, global, "delegate", delegate); + assert(e == 0); + + // Exercise import.meta during warmup: a module reads import.meta.answer + // (populated by the meta callback) and stashes it on the global. This proves + // the producer installs the import.meta callback, and the value rides into the + // snapshot. The module is deleted before serializing so it leaves no global + // handles behind. + js_value_t *source; + e = js_create_string_utf8(env, (utf8_t *) "globalThis.meta = import.meta.answer", -1, &source); + assert(e == 0); + + js_module_t *module; + e = js_create_module(env, "warmup.js", -1, 0, source, on_meta, NULL, &module); + assert(e == 0); + + e = js_instantiate_module(env, module, NULL, NULL); + assert(e == 0); + + js_value_t *promise; + e = js_run_module(env, module, &promise); + assert(e == 0); + + js_promise_state_t state; + e = js_get_promise_state(env, promise, &state); + assert(e == 0); + + assert(state == js_promise_fulfilled); + + e = js_delete_module(env, module); + assert(e == 0); + + // A module that survives the snapshot: created, instantiated, and evaluated, + // then left in place so its compiled record is serialized. Its id is stashed on + // the global so the consumer can look it up. + js_value_t *survivor_source; + e = js_create_string_utf8(env, (utf8_t *) "export const answer = 42", -1, &survivor_source); + assert(e == 0); + + js_module_t *survivor; + e = js_create_module(env, "survivor.js", -1, 0, survivor_source, NULL, NULL, &survivor); + assert(e == 0); + + e = js_instantiate_module(env, survivor, NULL, NULL); + assert(e == 0); + + js_value_t *survivor_promise; + e = js_run_module(env, survivor, &survivor_promise); + assert(e == 0); + + js_promise_state_t survivor_state; + e = js_get_promise_state(env, survivor_promise, &survivor_state); + assert(e == 0); + + assert(survivor_state == js_promise_fulfilled); + + js_value_t *survivor_id; + e = js_get_module_id(env, survivor, &survivor_id); + assert(e == 0); + + e = js_set_named_property(env, global, "moduleId", survivor_id); + assert(e == 0); + + e = js_close_handle_scope(env, scope); + assert(e == 0); + + void *data; + size_t len; + e = js_take_snapshot(env, &data, &len); + assert(e == 0); + + // Write the blob to a temp file the child process boots from. + + char path[4096]; + size_t path_len = sizeof(path); + e = uv_os_tmpdir(path, &path_len); + assert(e == 0); + + strncat(path, "/libjs-boot-from-snapshot.blob", sizeof(path) - strlen(path) - 1); + + FILE *file = fopen(path, "wb"); + assert(file != NULL); + + assert(fwrite(data, 1, len, file) == len); + + fclose(file); + + free(data); + + // Re-spawn this executable in consume mode, pointed at the blob. + + char exepath[4096]; + size_t exepath_len = sizeof(exepath); + e = uv_exepath(exepath, &exepath_len); + assert(e == 0); + + e = uv_os_setenv("JS_SNAPSHOT_BLOB", path); + assert(e == 0); + + char *args[] = {exepath, NULL}; + + uv_process_options_t process_options = {0}; + process_options.file = exepath; + process_options.args = args; + process_options.exit_cb = on_exit; + + uv_process_t process; + e = uv_spawn(loop, &process, &process_options); + assert(e == 0); + + e = uv_run(loop, UV_RUN_DEFAULT); + assert(e == 0); + + remove(path); + + assert(exit_status == 0); +} + +static void +consume(uv_loop_t *loop, js_platform_t *platform, const char *path) { + int e; + + FILE *file = fopen(path, "rb"); + assert(file != NULL); + + fseek(file, 0, SEEK_END); + long len = ftell(file); + fseek(file, 0, SEEK_SET); + + void *data = malloc(len); + assert(fread(data, 1, len, file) == (size_t) len); + + fclose(file); + + js_rebind_handlers_t *handlers; + e = js_create_rebind_handlers(&handlers); + assert(e == 0); + + e = js_add_rebind_handler(handlers, rebind, NULL); + assert(e == 0); + + js_env_options_t options = { + .version = 1, + .snapshot = data, + .snapshot_len = (size_t) len, + .external_references = references(), + .rebind_handlers = handlers, + }; + + js_env_t *env; + e = js_create_env(loop, platform, &options, &env); + assert(e == 0); + + js_handle_scope_t *scope; + e = js_open_handle_scope(env, &scope); + assert(e == 0); + + js_value_t *global; + e = js_get_global(env, &global); + assert(e == 0); + + js_value_t *fn; + e = js_get_named_property(env, global, "answer", &fn); + assert(e == 0); + + js_value_t *result; + e = js_call_function(env, global, fn, 0, NULL, &result); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, result, &value); + assert(e == 0); + + assert(value == 42); + + // The import.meta value computed during warmup survived the snapshot. + js_value_t *meta; + e = js_get_named_property(env, global, "meta", &meta); + assert(e == 0); + + int32_t meta_value; + e = js_get_value_int32(env, meta, &meta_value); + assert(e == 0); + + assert(meta_value == 7); + + // The wrapped object survived: its finalizer side-table slot was rebuilt and + // the data rebound, so js_unwrap returns it. + js_value_t *wrapped; + e = js_get_named_property(env, global, "wrapped", &wrapped); + assert(e == 0); + + void *wrapped_data; + e = js_unwrap(env, wrapped, &wrapped_data); + assert(e == 0); + + assert((int32_t) (intptr_t) wrapped_data == 99); + + // The delegate survived: its internal field was reconstructed and data rebound, + // so property access routes through the rebound getter. + js_value_t *delegate; + e = js_get_named_property(env, global, "delegate", &delegate); + assert(e == 0); + + js_value_t *delegated; + e = js_get_named_property(env, delegate, "anything", &delegated); + assert(e == 0); + + int32_t delegated_value; + e = js_get_value_int32(env, delegated, &delegated_value); + assert(e == 0); + + assert(delegated_value == 123); + + // The module graph survived: the evaluated module is rebuilt from the snapshot, + // looked up by the id stashed on the global, and its namespace still holds the + // evaluated export. + js_value_t *module_id; + e = js_get_named_property(env, global, "moduleId", &module_id); + assert(e == 0); + + js_module_t *module; + e = js_get_module_by_id(env, module_id, &module); + assert(e == 0); + + js_value_t *namespace; + e = js_get_module_namespace(env, module, &namespace); + assert(e == 0); + + js_value_t *module_answer; + e = js_get_named_property(env, namespace, "answer", &module_answer); + assert(e == 0); + + int32_t module_answer_value; + e = js_get_value_int32(env, module_answer, &module_answer_value); + assert(e == 0); + + assert(module_answer_value == 42); + + e = js_close_handle_scope(env, scope); + assert(e == 0); + + e = js_destroy_env(env); + assert(e == 0); + + e = js_destroy_rebind_handlers(handlers); + assert(e == 0); + + free(data); +} + +int +main() { + int e; + + uv_loop_t *loop = uv_default_loop(); + + js_platform_t *platform; + e = js_create_platform(loop, NULL, &platform); + assert(e == 0); + + char path[4096]; + size_t path_len = sizeof(path); + + if (uv_os_getenv("JS_SNAPSHOT_BLOB", path, &path_len) == 0) { + consume(loop, platform, path); + } else { + produce(loop, platform); + } + + e = js_destroy_platform(platform); + assert(e == 0); + + e = uv_run(loop, UV_RUN_DEFAULT); + assert(e == 0); +} diff --git a/test/create-snapshot.c b/test/create-snapshot.c new file mode 100644 index 0000000..f8f8fbc --- /dev/null +++ b/test/create-snapshot.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include + +#include "../include/js.h" + +// Validates the snapshot producer: warm up an environment, serialize it, and +// confirm a non-empty blob is handed back and the environment tears down +// cleanly. +// +// The blob is deliberately not booted in this same process: this build enables +// V8's shared read-only heap (a build-level setting), so loading a freshly +// produced blob in a process that already has a shared read-only heap trips a +// debug-only checksum CHECK. Snapshots are produced offline and consumed in a +// separate process, so full round-trip coverage belongs to a release or +// cross-process test. + +int +main() { + int e; + + uv_loop_t *loop = uv_default_loop(); + + js_platform_t *platform; + e = js_create_platform(loop, NULL, &platform); + assert(e == 0); + + js_env_t *env; + e = js_create_snapshot(loop, platform, NULL, &env); + assert(e == 0); + + js_handle_scope_t *scope; + e = js_open_handle_scope(env, &scope); + assert(e == 0); + + js_value_t *script; + e = js_create_string_utf8(env, (utf8_t *) "globalThis.answer = 42", -1, &script); + assert(e == 0); + + js_value_t *result; + e = js_run_script(env, NULL, 0, 0, script, &result); + assert(e == 0); + + e = js_close_handle_scope(env, scope); + assert(e == 0); + + void *data; + size_t len; + e = js_take_snapshot(env, &data, &len); + assert(e == 0); + + // `env` is consumed by `js_take_snapshot()` and must not be destroyed. + + assert(data != NULL); + assert(len > 0); + + free(data); + + e = js_destroy_platform(platform); + assert(e == 0); + + e = uv_run(loop, UV_RUN_DEFAULT); + assert(e == 0); +} diff --git a/test/external-references.c b/test/external-references.c new file mode 100644 index 0000000..ebb01f6 --- /dev/null +++ b/test/external-references.c @@ -0,0 +1,56 @@ +#include +#include +#include + +#include "../include/js.h" + +static void +fn_a() {} + +static void +fn_b() {} + +int +main() { + int e; + + js_external_references_t *references; + e = js_create_external_references(&references); + assert(e == 0); + + e = js_add_external_reference(references, (const void *) fn_a); + assert(e == 0); + + e = js_add_external_reference(references, (const void *) fn_b); + assert(e == 0); + + const intptr_t *data; + size_t len; + e = js_get_external_references(references, &data, &len); + assert(e == 0); + + assert(len == 2); + + assert(data[0] == (intptr_t) fn_a); + assert(data[1] == (intptr_t) fn_b); + + // The array handed to V8 must be null-terminated. + assert(data[len] == 0); + + // Adding after a read stays valid; the terminator is not left dangling in the + // middle of the array. + e = js_add_external_reference(references, (const void *) main); + assert(e == 0); + + e = js_get_external_references(references, &data, &len); + assert(e == 0); + + assert(len == 3); + assert(data[2] == (intptr_t) main); + assert(data[len] == 0); + + e = js_destroy_external_references(references); + assert(e == 0); + + return 0; +} diff --git a/test/fixtures/snapshots/.gitignore b/test/fixtures/snapshots/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/test/fixtures/snapshots/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/run-from-snapshot-delegate.c b/test/run-from-snapshot-delegate.c new file mode 100644 index 0000000..d7481bd --- /dev/null +++ b/test/run-from-snapshot-delegate.c @@ -0,0 +1,89 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that a delegate survives an isolate snapshot: the producer creates a +// delegate reachable from the global. After restore its internal field is +// reconstructed and its data rebound, so property access routes through the +// rebound getter, which returns its data. + +static js_value_t * +delegate_get(js_env_t *env, js_value_t *property, void *data) { + int e; + + js_value_t *result; + e = js_create_int32(env, (int32_t) (intptr_t) data, &result); + assert(e == 0); + + return result; +} + +static bool +rebind(js_env_t *env, const js_rebind_info_t *info, void *context, js_binding_payload_t *result) { + if (info->type == js_binding_delegate && info->delegate.callbacks.get == delegate_get) { + result->data = (void *) (intptr_t) 123; + + return true; + } + + return false; +} + +static void +references(js_external_references_t *references) { + int e; + + e = js_add_external_reference(references, (const void *) delegate_get); + assert(e == 0); +} + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_delegate_callbacks_t callbacks = { + .version = 0, + .get = delegate_get, + }; + + js_value_t *delegate; + e = js_create_delegate(env, &callbacks, (void *) (intptr_t) 123, NULL, NULL, &delegate); + assert(e == 0); + + e = js_set_named_property(env, global, "delegate", delegate); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *delegate; + e = js_get_named_property(env, global, "delegate", &delegate); + assert(e == 0); + + js_value_t *delegated; + e = js_get_named_property(env, delegate, "anything", &delegated); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, delegated, &value); + assert(e == 0); + + assert(value == 123); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-delegate", + .references = references, + .rebind = rebind, + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot-dynamic-import.c b/test/run-from-snapshot-dynamic-import.c new file mode 100644 index 0000000..1d216a8 --- /dev/null +++ b/test/run-from-snapshot-dynamic-import.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that dynamic import() works after booting from a snapshot: the +// producer warms up a plain global, and the consumer registers a dynamic import +// handler and runs a script that imports a module. The import is resolved +// asynchronously from a timer, so it only completes once the event loop runs, +// proving the restored environment drives dynamic import end to end. +// +// The import handler is set at runtime with js_on_dynamic_import() rather than +// serialized, so the consumer reinstates it after restore. + +static js_env_t *import_env; +static js_deferred_t *import_deferred; +static js_value_t *import_namespace; +static uv_timer_t import_timer; +static bool import_resolved; + +static void +on_module_evaluate(js_env_t *env, js_module_t *module, void *data) { + int e; + + js_value_t *name; + e = js_create_string_utf8(env, (utf8_t *) "answer", -1, &name); + assert(e == 0); + + js_value_t *value; + e = js_create_uint32(env, 42, &value); + assert(e == 0); + + e = js_set_module_export(env, module, name, value); + assert(e == 0); +} + +// Fires from the event loop and resolves the pending dynamic import, so the +// import only completes once the loop has run. +static void +on_timer(uv_timer_t *timer) { + int e; + + e = js_resolve_deferred(import_env, import_deferred, import_namespace); + assert(e == 0); + + import_resolved = true; + + uv_close((uv_handle_t *) timer, NULL); +} + +// Builds and evaluates the requested module synchronously, but defers handing +// back its namespace until a timer fires on the event loop. +static js_value_t * +on_import(js_env_t *env, js_value_t *specifier, js_value_t *assertions, js_value_t *referrer, js_value_t *id, void *data) { + int e; + + js_value_t *export_names[1]; + e = js_create_string_utf8(env, (utf8_t *) "answer", -1, &export_names[0]); + assert(e == 0); + + js_module_t *module; + e = js_create_synthetic_module(env, "synthetic", -1, export_names, 1, on_module_evaluate, NULL, &module); + assert(e == 0); + + e = js_instantiate_module(env, module, NULL, NULL); + assert(e == 0); + + js_value_t *evaluated; + e = js_run_module(env, module, &evaluated); + assert(e == 0); + + e = js_get_module_namespace(env, module, &import_namespace); + assert(e == 0); + + js_value_t *promise; + e = js_create_promise(env, &import_deferred, &promise); + assert(e == 0); + + import_env = env; + + uv_loop_t *loop; + e = js_get_env_loop(env, &loop); + assert(e == 0); + + e = uv_timer_init(loop, &import_timer); + assert(e == 0); + + e = uv_timer_start(&import_timer, on_timer, 0, 0); + assert(e == 0); + + return promise; +} + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + // Seed the slot the consumer overwrites once the import resolves, to prove the + // import ran after restore rather than during warmup. + js_value_t *value; + e = js_create_uint32(env, 0, &value); + assert(e == 0); + + e = js_set_named_property(env, global, "answer", value); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + e = js_on_dynamic_import(env, on_import, NULL); + assert(e == 0); + + js_value_t *script; + e = js_create_string_utf8(env, (utf8_t *) "import('foo.js').then((ns) => { globalThis.answer = ns.answer })", -1, &script); + assert(e == 0); + + js_value_t *result; + e = js_run_script(env, "test.js", -1, 0, script, &result); + assert(e == 0); + + // The import is still pending; it resolves only once the timer fires. + js_value_t *answer; + e = js_get_named_property(env, global, "answer", &answer); + assert(e == 0); + + uint32_t value; + e = js_get_value_uint32(env, answer, &value); + assert(e == 0); + + assert(value == 0); + + // Run the loop until the import resolves. + uv_loop_t *loop; + e = js_get_env_loop(env, &loop); + assert(e == 0); + + while (!import_resolved) { + e = uv_run(loop, UV_RUN_ONCE); + assert(e >= 0); + } + + e = js_get_named_property(env, global, "answer", &answer); + assert(e == 0); + + e = js_get_value_uint32(env, answer, &value); + assert(e == 0); + + assert(value == 42); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-dynamic-import", + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot-function.c b/test/run-from-snapshot-function.c new file mode 100644 index 0000000..f8cfc11 --- /dev/null +++ b/test/run-from-snapshot-function.c @@ -0,0 +1,88 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that a native function survives an isolate snapshot: the producer +// binds a native `answer()` onto the global, and the consumer calls it. The +// function returns its `data`, which is NULL until rebound, so the test only +// passes if the rebind handler reinstates it after restore. + +static js_value_t * +answer(js_env_t *env, js_callback_info_t *info) { + int e; + + void *data; + e = js_get_callback_info(env, info, NULL, NULL, NULL, &data); + assert(e == 0); + + js_value_t *result; + e = js_create_int32(env, (int32_t) (intptr_t) data, &result); + assert(e == 0); + + return result; +} + +static bool +rebind(js_env_t *env, const js_rebind_info_t *info, void *context, js_binding_payload_t *result) { + if (info->type == js_binding_function && info->function.cb == answer) { + result->data = (void *) (intptr_t) 42; + + return true; + } + + return false; +} + +static void +references(js_external_references_t *references) { + int e; + + e = js_add_external_reference(references, (const void *) answer); + assert(e == 0); +} + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *fn; + e = js_create_function(env, "answer", -1, answer, (void *) (intptr_t) 42, &fn); + assert(e == 0); + + e = js_set_named_property(env, global, "answer", fn); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *fn; + e = js_get_named_property(env, global, "answer", &fn); + assert(e == 0); + + js_value_t *result; + e = js_call_function(env, global, fn, 0, NULL, &result); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, result, &value); + assert(e == 0); + + assert(value == 42); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-function", + .references = references, + .rebind = rebind, + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot-import-meta.c b/test/run-from-snapshot-import-meta.c new file mode 100644 index 0000000..3606453 --- /dev/null +++ b/test/run-from-snapshot-import-meta.c @@ -0,0 +1,78 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that import.meta works during warmup and the value it produced rides +// into the snapshot: the producer evaluates a module that reads +// import.meta.answer (populated by the meta callback) and stashes it on the +// global. The module is deleted before serializing so it leaves no global +// handles behind. The consumer reads the stashed value. + +static void +on_meta(js_env_t *env, js_module_t *module, js_value_t *meta, void *data) { + int e; + + js_value_t *value; + e = js_create_int32(env, 7, &value); + assert(e == 0); + + e = js_set_named_property(env, meta, "answer", value); + assert(e == 0); +} + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *source; + e = js_create_string_utf8(env, (utf8_t *) "globalThis.meta = import.meta.answer", -1, &source); + assert(e == 0); + + js_module_t *module; + e = js_create_module(env, "warmup.js", -1, 0, source, on_meta, NULL, &module); + assert(e == 0); + + e = js_instantiate_module(env, module, NULL, NULL); + assert(e == 0); + + js_value_t *promise; + e = js_run_module(env, module, &promise); + assert(e == 0); + + js_promise_state_t state; + e = js_get_promise_state(env, promise, &state); + assert(e == 0); + + assert(state == js_promise_fulfilled); + + e = js_delete_module(env, module); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *meta; + e = js_get_named_property(env, global, "meta", &meta); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, meta, &value); + assert(e == 0); + + assert(value == 7); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-import-meta", + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot-module.c b/test/run-from-snapshot-module.c new file mode 100644 index 0000000..3036267 --- /dev/null +++ b/test/run-from-snapshot-module.c @@ -0,0 +1,82 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that the module graph survives an isolate snapshot: the producer +// creates, instantiates, and evaluates a module, leaving it in place so its +// compiled record is serialized, and stashes its id on the global. The consumer +// rebuilds the module from the snapshot, looks it up by id, and reads its +// evaluated export from the namespace. + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *source; + e = js_create_string_utf8(env, (utf8_t *) "export const answer = 42", -1, &source); + assert(e == 0); + + js_module_t *module; + e = js_create_module(env, "survivor.js", -1, 0, source, NULL, NULL, &module); + assert(e == 0); + + e = js_instantiate_module(env, module, NULL, NULL); + assert(e == 0); + + js_value_t *promise; + e = js_run_module(env, module, &promise); + assert(e == 0); + + js_promise_state_t state; + e = js_get_promise_state(env, promise, &state); + assert(e == 0); + + assert(state == js_promise_fulfilled); + + js_value_t *id; + e = js_get_module_id(env, module, &id); + assert(e == 0); + + e = js_set_named_property(env, global, "moduleId", id); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *id; + e = js_get_named_property(env, global, "moduleId", &id); + assert(e == 0); + + js_module_t *module; + e = js_get_module_by_id(env, id, &module); + assert(e == 0); + + js_value_t *namespace; + e = js_get_module_namespace(env, module, &namespace); + assert(e == 0); + + js_value_t *answer; + e = js_get_named_property(env, namespace, "answer", &answer); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, answer, &value); + assert(e == 0); + + assert(value == 42); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-module", + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot-wrap.c b/test/run-from-snapshot-wrap.c new file mode 100644 index 0000000..a6cc22b --- /dev/null +++ b/test/run-from-snapshot-wrap.c @@ -0,0 +1,75 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that a `js_wrap`'d object survives an isolate snapshot: the producer +// wraps an object reachable from the global with non-NULL data and a finalizer. +// After restore its finalizer side-table slot is rebuilt and the data rebound, +// so `js_unwrap` returns it. + +static void +on_wrap_finalize(js_env_t *env, void *data, void *finalize_hint) {} + +static bool +rebind(js_env_t *env, const js_rebind_info_t *info, void *context, js_binding_payload_t *result) { + if (info->type == js_binding_finalizer && info->finalizer.cb == on_wrap_finalize) { + result->data = (void *) (intptr_t) 99; + + return true; + } + + return false; +} + +static void +references(js_external_references_t *references) { + int e; + + e = js_add_external_reference(references, (const void *) on_wrap_finalize); + assert(e == 0); +} + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *wrapped; + e = js_create_object(env, &wrapped); + assert(e == 0); + + e = js_wrap(env, wrapped, (void *) (intptr_t) 99, on_wrap_finalize, NULL, NULL); + assert(e == 0); + + e = js_set_named_property(env, global, "wrapped", wrapped); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *wrapped; + e = js_get_named_property(env, global, "wrapped", &wrapped); + assert(e == 0); + + void *data; + e = js_unwrap(env, wrapped, &data); + assert(e == 0); + + assert((int32_t) (intptr_t) data == 99); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-wrap", + .references = references, + .rebind = rebind, + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/run-from-snapshot.c b/test/run-from-snapshot.c new file mode 100644 index 0000000..ff24d89 --- /dev/null +++ b/test/run-from-snapshot.c @@ -0,0 +1,47 @@ +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that plain JavaScript state survives an isolate snapshot: the +// producer runs a script that assigns a global, and the consumer reads it back. + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *script; + e = js_create_string_utf8(env, (utf8_t *) "globalThis.answer = 42", -1, &script); + assert(e == 0); + + js_value_t *result; + e = js_run_script(env, NULL, 0, 0, script, &result); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *answer; + e = js_get_named_property(env, global, "answer", &answer); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, answer, &value); + assert(e == 0); + + assert(value == 42); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot", + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} diff --git a/test/snapshot.h b/test/snapshot.h new file mode 100644 index 0000000..bf3f2b2 --- /dev/null +++ b/test/snapshot.h @@ -0,0 +1,302 @@ +#ifndef JS_TEST_SNAPSHOT_H +#define JS_TEST_SNAPSHOT_H + +#include +#include +#include +#include +#include + +#include "../include/js.h" + +// Shared harness for the snapshot round-trip tests. +// +// Each test warms up an environment in a producer process, serializes it to a +// blob, and re-spawns the same executable in consume mode to boot from the +// blob and assert the warmed-up state survived. +// +// Producer and consumer run in different processes because this build enables +// V8's shared read-only heap, which makes in-process consumption of a freshly +// produced blob trip a debug-only checksum CHECK. + +// Registers any external references the test needs (native callbacks reachable +// from the snapshot). Called in both the producer and the consumer. +typedef void (*snapshot_references_cb)(js_external_references_t *references); + +// Warms up the environment before it is serialized. `global` is the global +// object, already inside an open handle scope. +typedef void (*snapshot_produce_cb)(js_env_t *env, js_value_t *global); + +// Asserts the warmed-up state survived. `global` is the restored global object, +// already inside an open handle scope. +typedef void (*snapshot_consume_cb)(js_env_t *env, js_value_t *global); + +typedef struct { + // Distinguishes this test's blob file from other tests running in parallel. + const char *name; + + // Optional; registers native callbacks reachable from the snapshot. + snapshot_references_cb references; + + // Optional; rebinds per-instance `data` after restore. + js_rebind_cb rebind; + + snapshot_produce_cb produce; + snapshot_consume_cb consume; +} snapshot_test_t; + +static inline js_external_references_t * +snapshot__references(const snapshot_test_t *test) { + if (test->references == NULL) return NULL; + + int e; + + js_external_references_t *references; + e = js_create_external_references(&references); + assert(e == 0); + + test->references(references); + + return references; +} + +static inline void +snapshot__blob_path(const snapshot_test_t *test, char *path, size_t path_len) { + snprintf(path, path_len, "test/fixtures/snapshots/%s", test->name); +} + +static inline void * +snapshot__read(uv_loop_t *loop, const char *path, size_t *len) { + int e; + + uv_fs_t req; + + int fd = uv_fs_open(loop, &req, path, UV_FS_O_RDONLY, 0, NULL); + assert(fd >= 0); + + uv_fs_req_cleanup(&req); + + e = uv_fs_fstat(loop, &req, fd, NULL); + assert(e == 0); + + size_t size = (size_t) req.statbuf.st_size; + + uv_fs_req_cleanup(&req); + + void *data = malloc(size); + assert(data != NULL); + + size_t offset = 0; + + while (offset < size) { + uv_buf_t buf = uv_buf_init((char *) data + offset, size - offset); + + e = uv_fs_read(loop, &req, fd, &buf, 1, offset, NULL); + assert(e > 0); + + uv_fs_req_cleanup(&req); + + offset += (size_t) e; + } + + e = uv_fs_close(loop, &req, fd, NULL); + assert(e == 0); + + uv_fs_req_cleanup(&req); + + *len = size; + + return data; +} + +static inline void +snapshot__write(uv_loop_t *loop, const char *path, void *data, size_t len) { + int e; + + uv_fs_t req; + + int fd = uv_fs_open(loop, &req, path, UV_FS_O_WRONLY | UV_FS_O_CREAT | UV_FS_O_TRUNC, 0644, NULL); + assert(fd >= 0); + + uv_fs_req_cleanup(&req); + + size_t offset = 0; + + while (offset < len) { + uv_buf_t buf = uv_buf_init((char *) data + offset, len - offset); + + e = uv_fs_write(loop, &req, fd, &buf, 1, offset, NULL); + assert(e > 0); + + uv_fs_req_cleanup(&req); + + offset += (size_t) e; + } + + e = uv_fs_close(loop, &req, fd, NULL); + assert(e == 0); + + uv_fs_req_cleanup(&req); +} + +static int snapshot__exit_status = -1; + +static inline void +snapshot__on_exit(uv_process_t *process, int64_t status, int signal) { + snapshot__exit_status = (int) status; + + uv_close((uv_handle_t *) process, NULL); +} + +static inline void +snapshot__produce(const snapshot_test_t *test, uv_loop_t *loop, js_platform_t *platform) { + int e; + + js_snapshot_options_t options = { + .version = 0, + .external_references = snapshot__references(test), + }; + + js_env_t *env; + e = js_create_snapshot(loop, platform, &options, &env); + assert(e == 0); + + js_handle_scope_t *scope; + e = js_open_handle_scope(env, &scope); + assert(e == 0); + + js_value_t *global; + e = js_get_global(env, &global); + assert(e == 0); + + test->produce(env, global); + + e = js_close_handle_scope(env, scope); + assert(e == 0); + + void *data; + size_t len; + e = js_take_snapshot(env, &data, &len); + assert(e == 0); + + // Write the blob to a file the child process boots from. + + char path[4096]; + snapshot__blob_path(test, path, sizeof(path)); + + snapshot__write(loop, path, data, len); + + free(data); + + // Re-spawn this executable in consume mode, pointed at the blob. + + char exepath[4096]; + size_t exepath_len = sizeof(exepath); + e = uv_exepath(exepath, &exepath_len); + assert(e == 0); + + e = uv_os_setenv("SNAPSHOT_BLOB", path); + assert(e == 0); + + char *args[] = {exepath, NULL}; + + uv_process_options_t process_options = { + .file = exepath, + .args = args, + .exit_cb = snapshot__on_exit, + }; + + uv_process_t process; + e = uv_spawn(loop, &process, &process_options); + assert(e == 0); + + e = uv_run(loop, UV_RUN_DEFAULT); + assert(e == 0); + + // The blob is left in place so it can be inspected after the test runs. + + assert(snapshot__exit_status == 0); +} + +static inline void +snapshot__consume(const snapshot_test_t *test, uv_loop_t *loop, js_platform_t *platform, const char *path) { + int e; + + size_t len; + void *data = snapshot__read(loop, path, &len); + + js_rebind_handlers_t *handlers = NULL; + + if (test->rebind != NULL) { + e = js_create_rebind_handlers(&handlers); + assert(e == 0); + + e = js_add_rebind_handler(handlers, test->rebind, NULL); + assert(e == 0); + } + + js_env_options_t options = { + .version = 1, + .snapshot = data, + .snapshot_len = len, + .external_references = snapshot__references(test), + .rebind_handlers = handlers, + }; + + js_env_t *env; + e = js_create_env(loop, platform, &options, &env); + assert(e == 0); + + js_handle_scope_t *scope; + e = js_open_handle_scope(env, &scope); + assert(e == 0); + + js_value_t *global; + e = js_get_global(env, &global); + assert(e == 0); + + test->consume(env, global); + + e = js_close_handle_scope(env, scope); + assert(e == 0); + + e = js_destroy_env(env); + assert(e == 0); + + if (handlers != NULL) { + e = js_destroy_rebind_handlers(handlers); + assert(e == 0); + } + + free(data); +} + +// Runs the producer, or the consumer if booted with `SNAPSHOT_BLOB` set +// (as the producer re-spawns itself). +static inline void +snapshot_test_run(const snapshot_test_t *test) { + int e; + + uv_loop_t *loop = uv_default_loop(); + + js_platform_t *platform; + e = js_create_platform(loop, NULL, &platform); + assert(e == 0); + + char path[4096]; + size_t path_len = sizeof(path); + + if (uv_os_getenv("SNAPSHOT_BLOB", path, &path_len) == 0) { + snapshot__consume(test, loop, platform, path); + } else { + snapshot__produce(test, loop, platform); + } + + e = js_destroy_platform(platform); + assert(e == 0); + + e = uv_run(loop, UV_RUN_DEFAULT); + assert(e == 0); +} + +#endif // JS_TEST_SNAPSHOT_H From 29cfed550992f5b57076b60dbb4bd00f7f06f94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 29 Jun 2026 13:40:20 +0200 Subject: [PATCH 2/3] Support prepared script snapshotting --- include/js.h | 8 +++ src/js.cc | 101 ++++++++++++++++++++++++++++++-- test/CMakeLists.txt | 1 + test/run-from-snapshot-script.c | 77 ++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 test/run-from-snapshot-script.c diff --git a/include/js.h b/include/js.h index e9b653f..1f39238 100644 --- a/include/js.h +++ b/include/js.h @@ -765,6 +765,14 @@ js_get_script_name(js_env_t *env, js_script_t *script, const char **result); int js_get_script_id(js_env_t *env, js_script_t *script, js_value_t **result); +/** + * Look up a prepared script reconstructed from a snapshot by its `id` and return + * its handle. A prepared script carries no callbacks or per-instance state, so + * nothing needs to be re-supplied after restore. + */ +int +js_get_script_by_id(js_env_t *env, js_value_t *id, js_script_t **result); + int js_create_module(js_env_t *env, const char *name, size_t len, int offset, js_value_t *source, js_module_meta_cb cb, void *data, js_module_t **result); diff --git a/src/js.cc b/src/js.cc index b0b506c..1fd3bda 100644 --- a/src/js.cc +++ b/src/js.cc @@ -1495,6 +1495,8 @@ struct js_env_s { std::multimap modules; + std::list scripts; + Global default_module_id; std::list> unhandled_promises; @@ -1535,6 +1537,7 @@ struct js_env_s { tag(), exception(), modules(), + scripts(), default_module_id(), unhandled_promises(), teardown_queue(), @@ -1632,8 +1635,6 @@ struct js_env_s { context.Get(isolate)->Exit(); context.Reset(); - - isolate->Exit(); } isolate->Dispose(); @@ -3643,20 +3644,23 @@ namespace { // this order) and `js_rebind_from_snapshot` (which reads it back). The metadata // array and the wrapper symbol occupy fixed slots; each serialized module record // follows in iteration order, so module `i` lands at `js_snapshot_data_modules -// + i`. +// + i`. Each prepared script's `UnboundScript` follows the modules, so script +// `j` lands at `js_snapshot_data_modules + module_count + j`; the consumer +// recovers `module_count` from the length of the module id list. enum { js_snapshot_data_metadata = 0, js_snapshot_data_wrapper = 1, js_snapshot_data_modules = 2, }; -// Metadata array layout: the manifests and id list `js_rebind_from_snapshot` +// Metadata array layout: the manifests and id lists `js_rebind_from_snapshot` // replays, indexed within the metadata array stored at // `js_snapshot_data_metadata`. enum { js_snapshot_metadata_functions = 0, js_snapshot_metadata_wraps = 1, js_snapshot_metadata_module_ids = 2, + js_snapshot_metadata_script_ids = 3, }; // Asks each handler, in order, to claim a reconstructed binding - described by @@ -3918,6 +3922,41 @@ js_rebind_from_snapshot(js_env_t *env, const js_rebind_handlers_t *handlers) { env->modules.emplace(module->GetIdentityHash(), record); } + + // Prepared scripts: rebuild a `js_script_t` for each serialized `UnboundScript`, + // paired by index with its id symbol. A prepared script carries no callbacks + // and no per-instance `data`, so there is nothing to rebind; the embedder + // reconnects its handles to the records by id via `js_get_script_by_id`. The + // records follow the modules in the data slots, so they begin at + // `js_snapshot_data_modules + module_count`. + auto scripts = metadata->Get(context, js_snapshot_metadata_script_ids).ToLocalChecked().As(); + + auto script_base = js_snapshot_data_modules + modules->Length(); + + for (uint32_t j = 0; j < scripts->Length(); j++) { + auto id = scripts->Get(context, j).ToLocalChecked().As(); + + Local data; + if (!context->GetDataFromSnapshotOnce(script_base + j).ToLocal(&data)) continue; + + auto script = *reinterpret_cast *>(&data); + + // Recover the name from the script's resource name (the same string passed + // as the file name at preparation), exactly as modules recover theirs. + std::string name; + + auto resource = script->GetScriptName(); + if (!resource.IsEmpty() && resource->IsString()) { + auto string = resource.As(); + auto length = string->Utf8LengthV2(isolate); + name.resize(length); + string->WriteUtf8V2(isolate, name.data(), length, String::WriteFlags::kReplaceInvalidUtf8); + } + + auto record = new js_script_t(isolate, script, id, std::move(name)); + + env->scripts.push_back(record); + } } } // namespace @@ -4167,10 +4206,26 @@ js_take_snapshot(js_env_t *env, void **data, size_t *len) { module_records.push_back(record); } + // Script ids: the id symbol of every prepared script, in iteration order. + // Each script's compiled `UnboundScript` is added below as snapshot data at + // index `js_snapshot_data_modules + module_count + j`, so the consumer pairs + // `ids[j]` with the script at the same offset. + auto scripts = Array::New(isolate); + + std::vector> script_records; + + uint32_t sn = 0; + + for (auto script : env->scripts) { + scripts->Set(context, sn++, script->id.Get(isolate)).Check(); + script_records.push_back(script->script.Get(isolate)); + } + auto metadata = Array::New(isolate); metadata->Set(context, js_snapshot_metadata_functions, functions).Check(); metadata->Set(context, js_snapshot_metadata_wraps, wraps).Check(); metadata->Set(context, js_snapshot_metadata_module_ids, modules).Check(); + metadata->Set(context, js_snapshot_metadata_script_ids, scripts).Check(); // The wrapper symbol is a `Private`, not a `Value`, so it rides its own data // slot rather than the metadata array. @@ -4192,6 +4247,11 @@ js_take_snapshot(js_env_t *env, void **data, size_t *len) { auto module_index = creator->AddData(context, module_records[i]); assert(module_index == size_t(js_snapshot_data_modules) + i); } + + for (size_t j = 0; j < script_records.size(); j++) { + auto script_index = creator->AddData(context, script_records[j]); + assert(script_index == size_t(js_snapshot_data_modules) + module_records.size() + j); + } } // Release every Global into the isolate before serializing: `CreateBlob()` @@ -4219,6 +4279,11 @@ js_take_snapshot(js_env_t *env, void **data, size_t *len) { entry.second->id.Reset(); } + for (auto script : env->scripts) { + script->script.Reset(); + script->id.Reset(); + } + // Must run outside any handle scope. auto blob = creator->CreateBlob(env->snapshot.function_code_handling); @@ -4500,6 +4565,10 @@ js_prepare_script(js_env_t *env, const char *file, size_t len, int offset, js_va auto script = new js_script_t(env->isolate, compiled.ToLocalChecked(), id, std::move(script_name)); + // Track the script so the snapshot producer can serialize its compiled record + // and reset its global handles before `CreateBlob()`. + env->scripts.push_back(script); + *result = script; return 0; @@ -4534,6 +4603,8 @@ js_delete_script(js_env_t *env, js_script_t *script) { js_env_scope_t env_scope(env); + env->scripts.remove(script); + delete script; return 0; @@ -4559,6 +4630,28 @@ js_get_script_id(js_env_t *env, js_script_t *script, js_value_t **result) { return 0; } +extern "C" int +js_get_script_by_id(js_env_t *env, js_value_t *id, js_script_t **result) { + if (env->is_exception_pending()) return js_error(env); + + int err; + + auto symbol = js_to_local(id); + + for (auto script : env->scripts) { + if (script->id.Get(env->isolate) == symbol) { + *result = script; + + return 0; + } + } + + err = js_throw_error(env, NULL, "Could not find script"); + assert(err == 0); + + return js_error(env); +} + extern "C" int js_create_module(js_env_t *env, const char *name, size_t len, int offset, js_value_t *source, js_module_meta_cb cb, void *data, js_module_t **result) { if (env->is_exception_pending()) return js_error(env); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3d7ef90..847d9fc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -235,6 +235,7 @@ list(APPEND tests run-from-snapshot-function run-from-snapshot-import-meta run-from-snapshot-module + run-from-snapshot-script run-from-snapshot-wrap run-module run-module-async diff --git a/test/run-from-snapshot-script.c b/test/run-from-snapshot-script.c new file mode 100644 index 0000000..48e8bb0 --- /dev/null +++ b/test/run-from-snapshot-script.c @@ -0,0 +1,77 @@ +#include +#include +#include +#include + +#include "../include/js.h" +#include "snapshot.h" + +// Verifies that a prepared script survives an isolate snapshot: the producer +// prepares (but does not run) a script and stashes its id on the global. The +// consumer rebuilds the script from the snapshot, looks it up by id, runs it, +// and asserts both its result and its preserved name. A prepared script carries +// no callbacks or per-instance data, so nothing needs to be rebound after +// restore. + +static void +produce(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *source; + e = js_create_string_utf8(env, (utf8_t *) "40 + 2", -1, &source); + assert(e == 0); + + js_script_t *script; + e = js_prepare_script(env, "survivor.js", -1, 0, source, &script); + assert(e == 0); + + js_value_t *id; + e = js_get_script_id(env, script, &id); + assert(e == 0); + + e = js_set_named_property(env, global, "scriptId", id); + assert(e == 0); +} + +static void +consume(js_env_t *env, js_value_t *global) { + int e; + + js_value_t *id; + e = js_get_named_property(env, global, "scriptId", &id); + assert(e == 0); + + js_script_t *script; + e = js_get_script_by_id(env, id, &script); + assert(e == 0); + + const char *name; + e = js_get_script_name(env, script, &name); + assert(e == 0); + + assert(strcmp(name, "survivor.js") == 0); + + js_value_t *result; + e = js_run_prepared_script(env, script, &result); + assert(e == 0); + + int32_t value; + e = js_get_value_int32(env, result, &value); + assert(e == 0); + + assert(value == 42); + + e = js_delete_script(env, script); + assert(e == 0); +} + +int +main() { + snapshot_test_t test = { + .name = "run-from-snapshot-script", + .produce = produce, + .consume = consume, + }; + + snapshot_test_run(&test); +} From d8f782a11d8a48e2a35838788a1a7333a107a068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 3 Jul 2026 08:29:25 +0200 Subject: [PATCH 3/3] Remove old test case --- test/boot-from-snapshot.c | 441 -------------------------------------- 1 file changed, 441 deletions(-) delete mode 100644 test/boot-from-snapshot.c diff --git a/test/boot-from-snapshot.c b/test/boot-from-snapshot.c deleted file mode 100644 index 4a23a8c..0000000 --- a/test/boot-from-snapshot.c +++ /dev/null @@ -1,441 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "../include/js.h" - -// Verifies that a native function survives an isolate snapshot: the producer -// binds a native `answer()` onto the global and serializes a blob; a separate -// process boots from the blob and calls `globalThis.answer()`, expecting 42. -// -// Producer and consumer run in different processes (the test re-spawns itself) -// because this build enables V8's shared read-only heap, which makes in-process -// consumption of a freshly produced blob trip a debug-only checksum CHECK. - -static js_value_t * -answer(js_env_t *env, js_callback_info_t *info) { - int e; - - // Return the callback's `data`, so the test only passes if `data` is rebound - // after restore (it would read NULL, and return 0, without a rebind handler). - void *data; - e = js_get_callback_info(env, info, NULL, NULL, NULL, &data); - assert(e == 0); - - js_value_t *result; - e = js_create_int32(env, (int32_t) (intptr_t) data, &result); - assert(e == 0); - - return result; -} - -static void -on_wrap_finalize(js_env_t *env, void *data, void *finalize_hint) {} - -// A delegate getter that returns its data for any property. -static js_value_t * -delegate_get(js_env_t *env, js_value_t *property, void *data) { - int e; - - js_value_t *result; - e = js_create_int32(env, (int32_t) (intptr_t) data, &result); - assert(e == 0); - - return result; -} - -static bool -rebind(js_env_t *env, js_binding_type_t type, const void *cb, js_value_t *holder, void *context, js_binding_payload_t *result) { - if (type == js_binding_function && cb == (const void *) answer) { - result->data = (void *) (intptr_t) 42; - - return true; - } - - if (type == js_binding_finalizer && cb == (const void *) on_wrap_finalize) { - result->data = (void *) (intptr_t) 99; - - return true; - } - - if (type == js_binding_delegate && cb == (const void *) delegate_get) { - result->data = (void *) (intptr_t) 123; - - return true; - } - - return false; -} - -static void -on_meta(js_env_t *env, js_module_t *module, js_value_t *meta, void *data) { - int e; - - js_value_t *value; - e = js_create_int32(env, 7, &value); - assert(e == 0); - - e = js_set_named_property(env, meta, "answer", value); - assert(e == 0); -} - -static js_external_references_t * -references() { - int e; - - js_external_references_t *references; - e = js_create_external_references(&references); - assert(e == 0); - - e = js_add_external_reference(references, (const void *) answer); - assert(e == 0); - - e = js_add_external_reference(references, (const void *) on_wrap_finalize); - assert(e == 0); - - e = js_add_external_reference(references, (const void *) delegate_get); - assert(e == 0); - - return references; -} - -static int exit_status = -1; - -static void -on_exit(uv_process_t *process, int64_t status, int signal) { - exit_status = (int) status; - - uv_close((uv_handle_t *) process, NULL); -} - -static void -produce(uv_loop_t *loop, js_platform_t *platform) { - int e; - - js_snapshot_options_t options = { - .version = 0, - .external_references = references(), - }; - - js_env_t *env; - e = js_create_snapshot(loop, platform, &options, &env); - assert(e == 0); - - js_handle_scope_t *scope; - e = js_open_handle_scope(env, &scope); - assert(e == 0); - - js_value_t *global; - e = js_get_global(env, &global); - assert(e == 0); - - js_value_t *fn; - e = js_create_function(env, "answer", -1, answer, (void *) (intptr_t) 42, &fn); - assert(e == 0); - - e = js_set_named_property(env, global, "answer", fn); - assert(e == 0); - - // Wrap an object reachable from the global, with non-NULL data and a finalizer. - // After restore its finalizer side-table slot is rebuilt and the data rebound. - js_value_t *wrapped; - e = js_create_object(env, &wrapped); - assert(e == 0); - - e = js_wrap(env, wrapped, (void *) (intptr_t) 99, on_wrap_finalize, NULL, NULL); - assert(e == 0); - - e = js_set_named_property(env, global, "wrapped", wrapped); - assert(e == 0); - - // Create a delegate reachable from the global. After restore its internal - // field is reconstructed and its data rebound, so property access routes - // through the rebound getter. - js_delegate_callbacks_t callbacks = { - .version = 0, - .get = delegate_get, - }; - - js_value_t *delegate; - e = js_create_delegate(env, &callbacks, (void *) (intptr_t) 123, NULL, NULL, &delegate); - assert(e == 0); - - e = js_set_named_property(env, global, "delegate", delegate); - assert(e == 0); - - // Exercise import.meta during warmup: a module reads import.meta.answer - // (populated by the meta callback) and stashes it on the global. This proves - // the producer installs the import.meta callback, and the value rides into the - // snapshot. The module is deleted before serializing so it leaves no global - // handles behind. - js_value_t *source; - e = js_create_string_utf8(env, (utf8_t *) "globalThis.meta = import.meta.answer", -1, &source); - assert(e == 0); - - js_module_t *module; - e = js_create_module(env, "warmup.js", -1, 0, source, on_meta, NULL, &module); - assert(e == 0); - - e = js_instantiate_module(env, module, NULL, NULL); - assert(e == 0); - - js_value_t *promise; - e = js_run_module(env, module, &promise); - assert(e == 0); - - js_promise_state_t state; - e = js_get_promise_state(env, promise, &state); - assert(e == 0); - - assert(state == js_promise_fulfilled); - - e = js_delete_module(env, module); - assert(e == 0); - - // A module that survives the snapshot: created, instantiated, and evaluated, - // then left in place so its compiled record is serialized. Its id is stashed on - // the global so the consumer can look it up. - js_value_t *survivor_source; - e = js_create_string_utf8(env, (utf8_t *) "export const answer = 42", -1, &survivor_source); - assert(e == 0); - - js_module_t *survivor; - e = js_create_module(env, "survivor.js", -1, 0, survivor_source, NULL, NULL, &survivor); - assert(e == 0); - - e = js_instantiate_module(env, survivor, NULL, NULL); - assert(e == 0); - - js_value_t *survivor_promise; - e = js_run_module(env, survivor, &survivor_promise); - assert(e == 0); - - js_promise_state_t survivor_state; - e = js_get_promise_state(env, survivor_promise, &survivor_state); - assert(e == 0); - - assert(survivor_state == js_promise_fulfilled); - - js_value_t *survivor_id; - e = js_get_module_id(env, survivor, &survivor_id); - assert(e == 0); - - e = js_set_named_property(env, global, "moduleId", survivor_id); - assert(e == 0); - - e = js_close_handle_scope(env, scope); - assert(e == 0); - - void *data; - size_t len; - e = js_take_snapshot(env, &data, &len); - assert(e == 0); - - // Write the blob to a temp file the child process boots from. - - char path[4096]; - size_t path_len = sizeof(path); - e = uv_os_tmpdir(path, &path_len); - assert(e == 0); - - strncat(path, "/libjs-boot-from-snapshot.blob", sizeof(path) - strlen(path) - 1); - - FILE *file = fopen(path, "wb"); - assert(file != NULL); - - assert(fwrite(data, 1, len, file) == len); - - fclose(file); - - free(data); - - // Re-spawn this executable in consume mode, pointed at the blob. - - char exepath[4096]; - size_t exepath_len = sizeof(exepath); - e = uv_exepath(exepath, &exepath_len); - assert(e == 0); - - e = uv_os_setenv("JS_SNAPSHOT_BLOB", path); - assert(e == 0); - - char *args[] = {exepath, NULL}; - - uv_process_options_t process_options = {0}; - process_options.file = exepath; - process_options.args = args; - process_options.exit_cb = on_exit; - - uv_process_t process; - e = uv_spawn(loop, &process, &process_options); - assert(e == 0); - - e = uv_run(loop, UV_RUN_DEFAULT); - assert(e == 0); - - remove(path); - - assert(exit_status == 0); -} - -static void -consume(uv_loop_t *loop, js_platform_t *platform, const char *path) { - int e; - - FILE *file = fopen(path, "rb"); - assert(file != NULL); - - fseek(file, 0, SEEK_END); - long len = ftell(file); - fseek(file, 0, SEEK_SET); - - void *data = malloc(len); - assert(fread(data, 1, len, file) == (size_t) len); - - fclose(file); - - js_rebind_handlers_t *handlers; - e = js_create_rebind_handlers(&handlers); - assert(e == 0); - - e = js_add_rebind_handler(handlers, rebind, NULL); - assert(e == 0); - - js_env_options_t options = { - .version = 1, - .snapshot = data, - .snapshot_len = (size_t) len, - .external_references = references(), - .rebind_handlers = handlers, - }; - - js_env_t *env; - e = js_create_env(loop, platform, &options, &env); - assert(e == 0); - - js_handle_scope_t *scope; - e = js_open_handle_scope(env, &scope); - assert(e == 0); - - js_value_t *global; - e = js_get_global(env, &global); - assert(e == 0); - - js_value_t *fn; - e = js_get_named_property(env, global, "answer", &fn); - assert(e == 0); - - js_value_t *result; - e = js_call_function(env, global, fn, 0, NULL, &result); - assert(e == 0); - - int32_t value; - e = js_get_value_int32(env, result, &value); - assert(e == 0); - - assert(value == 42); - - // The import.meta value computed during warmup survived the snapshot. - js_value_t *meta; - e = js_get_named_property(env, global, "meta", &meta); - assert(e == 0); - - int32_t meta_value; - e = js_get_value_int32(env, meta, &meta_value); - assert(e == 0); - - assert(meta_value == 7); - - // The wrapped object survived: its finalizer side-table slot was rebuilt and - // the data rebound, so js_unwrap returns it. - js_value_t *wrapped; - e = js_get_named_property(env, global, "wrapped", &wrapped); - assert(e == 0); - - void *wrapped_data; - e = js_unwrap(env, wrapped, &wrapped_data); - assert(e == 0); - - assert((int32_t) (intptr_t) wrapped_data == 99); - - // The delegate survived: its internal field was reconstructed and data rebound, - // so property access routes through the rebound getter. - js_value_t *delegate; - e = js_get_named_property(env, global, "delegate", &delegate); - assert(e == 0); - - js_value_t *delegated; - e = js_get_named_property(env, delegate, "anything", &delegated); - assert(e == 0); - - int32_t delegated_value; - e = js_get_value_int32(env, delegated, &delegated_value); - assert(e == 0); - - assert(delegated_value == 123); - - // The module graph survived: the evaluated module is rebuilt from the snapshot, - // looked up by the id stashed on the global, and its namespace still holds the - // evaluated export. - js_value_t *module_id; - e = js_get_named_property(env, global, "moduleId", &module_id); - assert(e == 0); - - js_module_t *module; - e = js_get_module_by_id(env, module_id, &module); - assert(e == 0); - - js_value_t *namespace; - e = js_get_module_namespace(env, module, &namespace); - assert(e == 0); - - js_value_t *module_answer; - e = js_get_named_property(env, namespace, "answer", &module_answer); - assert(e == 0); - - int32_t module_answer_value; - e = js_get_value_int32(env, module_answer, &module_answer_value); - assert(e == 0); - - assert(module_answer_value == 42); - - e = js_close_handle_scope(env, scope); - assert(e == 0); - - e = js_destroy_env(env); - assert(e == 0); - - e = js_destroy_rebind_handlers(handlers); - assert(e == 0); - - free(data); -} - -int -main() { - int e; - - uv_loop_t *loop = uv_default_loop(); - - js_platform_t *platform; - e = js_create_platform(loop, NULL, &platform); - assert(e == 0); - - char path[4096]; - size_t path_len = sizeof(path); - - if (uv_os_getenv("JS_SNAPSHOT_BLOB", path, &path_len) == 0) { - consume(loop, platform, path); - } else { - produce(loop, platform); - } - - e = js_destroy_platform(platform); - assert(e == 0); - - e = uv_run(loop, UV_RUN_DEFAULT); - assert(e == 0); -}