mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 07:33:16 +00:00
230 lines
9.1 KiB
Zig
230 lines
9.1 KiB
Zig
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const js = @import("js.zig");
|
|
const v8 = js.v8;
|
|
|
|
const log = @import("../../log.zig");
|
|
|
|
const bridge = @import("bridge.zig");
|
|
const Context = @import("Context.zig");
|
|
const Platform = @import("Platform.zig");
|
|
const Snapshot = @import("Snapshot.zig");
|
|
const Inspector = @import("Inspector.zig");
|
|
const ExecutionWorld = @import("ExecutionWorld.zig");
|
|
|
|
const JsApis = bridge.JsApis;
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
|
|
// The Env maps to a V8 isolate, which represents a isolated sandbox for
|
|
// executing JavaScript. The Env is where we'll define our V8 <-> Zig bindings,
|
|
// and it's where we'll start ExecutionWorlds, which actually execute JavaScript.
|
|
// The `S` parameter is arbitrary state. When we start an ExecutionWorld, an instance
|
|
// of S must be given. This instance is available to any Zig binding.
|
|
// The `types` parameter is a tuple of Zig structures we want to bind to V8.
|
|
const Env = @This();
|
|
|
|
allocator: Allocator,
|
|
|
|
platform: *const Platform,
|
|
|
|
// the global isolate
|
|
isolate: js.Isolate,
|
|
|
|
// just kept around because we need to free it on deinit
|
|
isolate_params: *v8.CreateParams,
|
|
|
|
context_id: usize,
|
|
|
|
// Global handles that need to be freed on deinit
|
|
eternal_function_templates: []v8.Eternal,
|
|
|
|
// Dynamic slice to avoid circular dependency on JsApis.len at comptime
|
|
templates: []*const v8.FunctionTemplate,
|
|
|
|
pub fn init(allocator: Allocator, platform: *const Platform, snapshot: *Snapshot) !Env {
|
|
var params = try allocator.create(v8.CreateParams);
|
|
errdefer allocator.destroy(params);
|
|
v8.v8__Isolate__CreateParams__CONSTRUCT(params);
|
|
params.snapshot_blob = @ptrCast(&snapshot.startup_data);
|
|
|
|
params.array_buffer_allocator = v8.v8__ArrayBuffer__Allocator__NewDefaultAllocator().?;
|
|
errdefer v8.v8__ArrayBuffer__Allocator__DELETE(params.array_buffer_allocator.?);
|
|
|
|
params.external_references = &snapshot.external_references;
|
|
|
|
var isolate = js.Isolate.init(params);
|
|
errdefer isolate.deinit();
|
|
|
|
v8.v8__Isolate__SetHostImportModuleDynamicallyCallback(isolate.handle, Context.dynamicModuleCallback);
|
|
v8.v8__Isolate__SetPromiseRejectCallback(isolate.handle, promiseRejectCallback);
|
|
v8.v8__Isolate__SetMicrotasksPolicy(isolate.handle, v8.kExplicit);
|
|
v8.v8__Isolate__SetFatalErrorHandler(isolate.handle, fatalCallback);
|
|
v8.v8__Isolate__SetOOMErrorHandler(isolate.handle, oomCallback);
|
|
|
|
isolate.enter();
|
|
errdefer isolate.exit();
|
|
|
|
v8.v8__Isolate__SetHostInitializeImportMetaObjectCallback(isolate.handle, Context.metaObjectCallback);
|
|
|
|
// Allocate arrays dynamically to avoid comptime dependency on JsApis.len
|
|
const eternal_function_templates = try allocator.alloc(v8.Eternal, JsApis.len);
|
|
errdefer allocator.free(eternal_function_templates);
|
|
|
|
const templates = try allocator.alloc(*const v8.FunctionTemplate, JsApis.len);
|
|
errdefer allocator.free(templates);
|
|
|
|
{
|
|
var temp_scope: js.HandleScope = undefined;
|
|
temp_scope.init(isolate);
|
|
defer temp_scope.deinit();
|
|
|
|
inline for (JsApis, 0..) |JsApi, i| {
|
|
JsApi.Meta.class_id = i;
|
|
const data = v8.v8__Isolate__GetDataFromSnapshotOnce(isolate.handle, snapshot.data_start + i);
|
|
const function_handle: *const v8.FunctionTemplate = @ptrCast(data);
|
|
// Make function template eternal
|
|
v8.v8__Eternal__New(isolate.handle, @ptrCast(function_handle), &eternal_function_templates[i]);
|
|
|
|
// Extract the local handle from the global for easy access
|
|
const eternal_ptr = v8.v8__Eternal__Get(&eternal_function_templates[i], isolate.handle);
|
|
templates[i] = @ptrCast(@alignCast(eternal_ptr.?));
|
|
}
|
|
}
|
|
|
|
return .{
|
|
.context_id = 0,
|
|
.isolate = isolate,
|
|
.platform = platform,
|
|
.allocator = allocator,
|
|
.templates = templates,
|
|
.isolate_params = params,
|
|
.eternal_function_templates = eternal_function_templates,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Env) void {
|
|
self.allocator.free(self.templates);
|
|
self.allocator.free(self.eternal_function_templates);
|
|
|
|
self.isolate.exit();
|
|
self.isolate.deinit();
|
|
v8.v8__ArrayBuffer__Allocator__DELETE(self.isolate_params.array_buffer_allocator.?);
|
|
self.allocator.destroy(self.isolate_params);
|
|
}
|
|
|
|
pub fn newInspector(self: *Env, arena: Allocator, ctx: anytype) !*Inspector {
|
|
const inspector = try arena.create(Inspector);
|
|
try Inspector.init(inspector, self.isolate.handle, ctx);
|
|
return inspector;
|
|
}
|
|
|
|
pub fn runMicrotasks(self: *const Env) void {
|
|
self.isolate.performMicrotasksCheckpoint();
|
|
}
|
|
|
|
pub fn pumpMessageLoop(self: *const Env) bool {
|
|
return v8.v8__Platform__PumpMessageLoop(self.platform.handle, self.isolate.handle, false);
|
|
}
|
|
|
|
pub fn runIdleTasks(self: *const Env) void {
|
|
v8.v8__Platform__RunIdleTasks(self.platform.handle, self.isolate.handle, 1);
|
|
}
|
|
pub fn newExecutionWorld(self: *Env) !ExecutionWorld {
|
|
return .{
|
|
.env = self,
|
|
.context = null,
|
|
.context_arena = ArenaAllocator.init(self.allocator),
|
|
};
|
|
}
|
|
|
|
// V8 doesn't immediately free memory associated with
|
|
// a Context, it's managed by the garbage collector. We use the
|
|
// `lowMemoryNotification` call on the isolate to encourage v8 to free
|
|
// any contexts which have been freed.
|
|
pub fn lowMemoryNotification(self: *Env) void {
|
|
var handle_scope: js.HandleScope = undefined;
|
|
handle_scope.init(self.isolate);
|
|
defer handle_scope.deinit();
|
|
self.isolate.lowMemoryNotification();
|
|
}
|
|
|
|
pub fn dumpMemoryStats(self: *Env) void {
|
|
const stats = self.isolate.getHeapStatistics();
|
|
std.debug.print(
|
|
\\ Total Heap Size: {d}
|
|
\\ Total Heap Size Executable: {d}
|
|
\\ Total Physical Size: {d}
|
|
\\ Total Available Size: {d}
|
|
\\ Used Heap Size: {d}
|
|
\\ Heap Size Limit: {d}
|
|
\\ Malloced Memory: {d}
|
|
\\ External Memory: {d}
|
|
\\ Peak Malloced Memory: {d}
|
|
\\ Number Of Native Contexts: {d}
|
|
\\ Number Of Detached Contexts: {d}
|
|
\\ Total Global Handles Size: {d}
|
|
\\ Used Global Handles Size: {d}
|
|
\\ Zap Garbage: {any}
|
|
\\
|
|
, .{ stats.total_heap_size, stats.total_heap_size_executable, stats.total_physical_size, stats.total_available_size, stats.used_heap_size, stats.heap_size_limit, stats.malloced_memory, stats.external_memory, stats.peak_malloced_memory, stats.number_of_native_contexts, stats.number_of_detached_contexts, stats.total_global_handles_size, stats.used_global_handles_size, stats.does_zap_garbage });
|
|
}
|
|
|
|
fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void {
|
|
const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?;
|
|
const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?;
|
|
const js_isolate = js.Isolate{ .handle = v8_isolate };
|
|
const ctx = Context.fromIsolate(js_isolate);
|
|
|
|
const local = js.Local{
|
|
.ctx = ctx,
|
|
.isolate = js_isolate,
|
|
.handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?,
|
|
.call_arena = ctx.call_arena,
|
|
};
|
|
|
|
const value =
|
|
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
|
|
// @HandleScope - no reason to create a js.Context here
|
|
local.valueHandleToString(v8_value, .{}) catch |err| @errorName(err)
|
|
else
|
|
"no value";
|
|
|
|
log.debug(.js, "unhandled rejection", .{
|
|
.value = value,
|
|
.stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
|
|
.note = "This should be updated to call window.unhandledrejection",
|
|
});
|
|
}
|
|
|
|
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {
|
|
const location = std.mem.span(c_location);
|
|
const message = std.mem.span(c_message);
|
|
log.fatal(.app, "V8 fatal callback", .{ .location = location, .message = message });
|
|
@import("../../crash_handler.zig").crash("Fatal V8 Error", .{ .location = location, .message = message }, @returnAddress());
|
|
}
|
|
|
|
fn oomCallback(c_location: [*c]const u8, details: ?*const v8.OOMDetails) callconv(.c) void {
|
|
const location = std.mem.span(c_location);
|
|
const detail = if (details) |d| std.mem.span(d.detail) else "";
|
|
log.fatal(.app, "V8 OOM", .{ .location = location, .detail = detail });
|
|
@import("../../crash_handler.zig").crash("V8 OOM", .{ .location = location, .detail = detail }, @returnAddress());
|
|
}
|