diff --git a/include/js.h b/include/js.h index 8c603f7..e956c20 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; @@ -41,6 +42,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 { /** @@ -181,6 +186,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 { @@ -267,7 +273,7 @@ struct js_platform_limits_s { size_t string_length; }; -/** @version 0 */ +/** @version 1 */ struct js_env_options_s { int version; @@ -278,6 +284,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 */ @@ -334,6 +408,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 */ @@ -449,12 +565,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. @@ -639,6 +793,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); @@ -694,6 +856,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 9376137..e431715 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; @@ -107,10 +113,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 @@ -1401,6 +1412,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; @@ -1431,6 +1495,8 @@ struct js_env_s { std::multimap modules; + std::list scripts; + Global default_module_id; std::list> unhandled_promises; @@ -1450,7 +1516,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(), @@ -1467,11 +1537,14 @@ struct js_env_s { tag(), exception(), modules(), + scripts(), default_module_id(), unhandled_promises(), teardown_queue(), inspector(), - callbacks() { + callbacks(), + bindings(), + snapshot(std::move(references)) { int err; std::unique_lock guard(platform->lock); @@ -1517,7 +1590,20 @@ struct js_env_s { auto isolate_scope = Isolate::Scope(isolate); 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)); @@ -1528,7 +1614,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 isolate_scope = Isolate::Scope(isolate); auto scope = HandleScope(isolate); @@ -2238,36 +2333,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 @@ -2275,7 +2385,7 @@ struct js_callback_s { return FunctionTemplate::New( isolate, on_call, - external.Get(isolate), + Integer::NewFromUnsigned(isolate, index), signature, 0, ConstructorBehavior::kAllow, @@ -2283,12 +2393,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))); @@ -2305,6 +2421,7 @@ struct js_callback_s { } } +protected: static void on_finalize(const WeakCallbackInfo &info) { auto callback = info.GetParameter(); @@ -2313,6 +2430,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; @@ -2336,7 +2466,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, @@ -2347,22 +2477,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; @@ -2408,7 +2546,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; @@ -2417,38 +2557,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); @@ -2487,7 +2631,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); @@ -2518,7 +2662,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); @@ -2548,7 +2692,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); @@ -3419,6 +3563,405 @@ 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`. 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 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 +// 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); + } + + // 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 + 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; @@ -3442,6 +3985,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); @@ -3466,7 +4036,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; @@ -3480,6 +4058,250 @@ 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); + } + + // 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. + 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); + } + + 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()` + // 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(); + } + + 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); + + *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; @@ -3778,6 +4600,10 @@ js_prepare_script_with_code_cache(js_env_t *env, const char *file, size_t len, i 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; @@ -3848,6 +4674,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; @@ -3873,6 +4701,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) { return js_create_module_with_code_cache(env, name, len, offset, source, nullptr, 0, nullptr, cb, data, result); @@ -4114,6 +4964,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 @@ -4477,11 +5351,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); } ); @@ -4510,15 +5386,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; @@ -4537,17 +5415,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(); @@ -4568,15 +5448,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; } @@ -5356,6 +6242,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; @@ -7601,7 +8489,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; @@ -7613,10 +8501,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 1435687..8060c11 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -91,6 +91,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 @@ -140,6 +141,7 @@ list(APPEND tests dynamic-import-with-referrer-script dynamic-import-without-handler escapable-handle-scope + external-references fatal-exception freeze garbage-collection-tracking @@ -236,6 +238,14 @@ list(APPEND tests queue-microtask queue-microtask-with-callback 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-script + run-from-snapshot-wrap run-module run-module-async run-module-cyclic-import 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/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-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); +} 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