Skip to content

Commit 29671ac

Browse files
Merge pull request #847 from lightpanda-io/name-property-handler
enable conditionnal loading for polyfill
2 parents 72083c8 + 2cdc9e9 commit 29671ac

File tree

9 files changed

+138
-59
lines changed

9 files changed

+138
-59
lines changed

src/browser/page.zig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ pub const Page = struct {
9595

9696
state_pool: *std.heap.MemoryPool(State),
9797

98+
polyfill_loader: polyfill.Loader = .{},
99+
98100
pub fn init(self: *Page, arena: Allocator, session: *Session) !void {
99101
const browser = session.browser;
100102
self.* = .{
@@ -117,10 +119,7 @@ pub const Page = struct {
117119
}),
118120
.main_context = undefined,
119121
};
120-
self.main_context = try session.executor.createJsContext(&self.window, self, self, true);
121-
122-
// load polyfills
123-
try polyfill.load(self.arena, self.main_context);
122+
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
124123

125124
// message loop must run only non-test env
126125
if (comptime !builtin.is_test) {

src/browser/polyfill/fetch.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test "Browser.fetch" {
1616
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
1717
defer runner.deinit();
1818

19-
try @import("polyfill.zig").load(testing.allocator, runner.page.main_context);
19+
try @import("polyfill.zig").Loader.load("fetch", source, runner.page.main_context);
2020

2121
try runner.testCases(&.{
2222
.{

src/browser/polyfill/polyfill.zig

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,69 @@ const log = @import("../../log.zig");
2323
const Allocator = std.mem.Allocator;
2424
const Env = @import("../env.zig").Env;
2525

26-
const modules = [_]struct {
27-
name: []const u8,
28-
source: []const u8,
29-
}{
30-
.{ .name = "polyfill-fetch", .source = @import("fetch.zig").source },
31-
};
26+
pub const Loader = struct {
27+
state: enum { empty, loading } = .empty,
28+
29+
done: struct {
30+
fetch: bool = false,
31+
} = .{},
3232

33-
pub fn load(allocator: Allocator, js_context: *Env.JsContext) !void {
34-
var try_catch: Env.TryCatch = undefined;
35-
try_catch.init(js_context);
36-
defer try_catch.deinit();
33+
pub fn load(name: []const u8, source: []const u8, js_context: *Env.JsContext) !void {
34+
var try_catch: Env.TryCatch = undefined;
35+
try_catch.init(js_context);
36+
defer try_catch.deinit();
3737

38-
for (modules) |m| {
39-
_ = js_context.exec(m.source, m.name) catch |err| {
40-
if (try try_catch.err(allocator)) |msg| {
41-
defer allocator.free(msg);
42-
log.fatal(.app, "polyfill error", .{ .name = m.name, .err = msg });
38+
_ = js_context.exec(source, name) catch |err| {
39+
if (try try_catch.err(js_context.call_arena)) |msg| {
40+
log.fatal(.app, "polyfill error", .{ .name = name, .err = msg });
4341
}
4442
return err;
4543
};
4644
}
47-
}
45+
46+
pub fn missing(self: *Loader, name: []const u8, js_context: *Env.JsContext) bool {
47+
// Avoid recursive calls during polyfill loading.
48+
if (self.state == .loading) {
49+
return false;
50+
}
51+
52+
if (!self.done.fetch and isFetch(name)) {
53+
self.state = .loading;
54+
defer self.state = .empty;
55+
56+
const _name = "fetch";
57+
const source = @import("fetch.zig").source;
58+
log.debug(.polyfill, "dynamic load", .{ .property = name });
59+
load(_name, source, js_context) catch |err| {
60+
log.fatal(.app, "polyfill load", .{ .name = name, .err = err });
61+
};
62+
63+
// load the polyfill once.
64+
self.done.fetch = true;
65+
66+
// We return false here: We want v8 to continue the calling chain
67+
// to finally find the polyfill we just inserted. If we want to
68+
// return false and stops the call chain, we have to use
69+
// `info.GetReturnValue.Set()` function, or `undefined` will be
70+
// returned immediately.
71+
return false;
72+
}
73+
74+
if (comptime builtin.mode == .Debug) {
75+
log.debug(.unknown_prop, "unkown global property", .{
76+
.info = "but the property can exist in pure JS",
77+
.property = name,
78+
});
79+
}
80+
81+
return false;
82+
}
83+
84+
fn isFetch(name: []const u8) bool {
85+
if (std.mem.eql(u8, name, "fetch")) return true;
86+
if (std.mem.eql(u8, name, "Request")) return true;
87+
if (std.mem.eql(u8, name, "Response")) return true;
88+
if (std.mem.eql(u8, name, "Headers")) return true;
89+
return false;
90+
}
91+
};

src/cdp/cdp.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const Inspector = @import("../browser/env.zig").Env.Inspector;
3030
const Incrementing = @import("../id.zig").Incrementing;
3131
const Notification = @import("../notification.zig").Notification;
3232

33+
const polyfill = @import("../browser/polyfill/polyfill.zig");
34+
3335
pub const URL_BASE = "chrome://newtab/";
3436
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
3537

@@ -554,6 +556,10 @@ const IsolatedWorld = struct {
554556
executor: Env.ExecutionWorld,
555557
grant_universal_access: bool,
556558

559+
// Polyfill loader for the isolated world.
560+
// We want to load polyfill in the world's context.
561+
polyfill_loader: polyfill.Loader = .{},
562+
557563
pub fn deinit(self: *IsolatedWorld) void {
558564
self.executor.deinit();
559565
}
@@ -569,7 +575,13 @@ const IsolatedWorld = struct {
569575
// Currently we have only 1 page/frame and thus also only 1 state in the isolate world.
570576
pub fn createContext(self: *IsolatedWorld, page: *Page) !void {
571577
if (self.executor.js_context != null) return error.Only1IsolatedContextSupported;
572-
_ = try self.executor.createJsContext(&page.window, page, {}, false);
578+
_ = try self.executor.createJsContext(
579+
&page.window,
580+
page,
581+
{},
582+
false,
583+
Env.GlobalMissingCallback.init(&self.polyfill_loader),
584+
);
573585
}
574586
};
575587

src/cdp/domains/page.zig

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,6 @@ pub fn pageCreated(bc: anytype, page: *Page) !void {
284284
if (bc.isolated_world) |*isolated_world| {
285285
// We need to recreate the isolated world context
286286
try isolated_world.createContext(page);
287-
288-
const polyfill = @import("../../browser/polyfill/polyfill.zig");
289-
try polyfill.load(bc.arena, &isolated_world.executor.js_context.?);
290287
}
291288
}
292289

src/log.zig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ pub const Scope = enum {
3939
unknown_prop,
4040
web_api,
4141
xhr,
42+
polyfill,
4243
};
4344

4445
const Opts = struct {
4546
format: Format = if (is_debug) .pretty else .logfmt,
4647
level: Level = if (is_debug) .info else .warn,
47-
filter_scopes: []const Scope = &.{.unknown_prop},
48+
filter_scopes: []const Scope = &.{},
4849
};
4950

5051
pub var opts = Opts{};

src/main_wpt.zig

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ fn run(
126126
});
127127
defer runner.deinit();
128128

129-
try polyfill.load(arena, runner.page.main_context);
130-
131129
// loop over the scripts.
132130
const doc = parser.documentHTMLToDocument(runner.page.window.document);
133131
const scripts = try parser.documentGetElementsByTagName(doc, "script");

src/runtime/js.zig

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
370370
// when the handle_scope is freed.
371371
// We also maintain our own "context_arena" which allows us to have
372372
// all page related memory easily managed.
373-
pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool) !*JsContext {
373+
pub fn createJsContext(self: *ExecutionWorld, global: anytype, state: State, module_loader: anytype, enter: bool, global_callback: ?GlobalMissingCallback) !*JsContext {
374374
std.debug.assert(self.js_context == null);
375375

376376
const ModuleLoader = switch (@typeInfo(@TypeOf(module_loader))) {
@@ -399,6 +399,30 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
399399
const global_template = js_global.getInstanceTemplate();
400400
global_template.setInternalFieldCount(1);
401401

402+
// Configure the missing property callback on the global
403+
// object.
404+
if (global_callback != null) {
405+
const configuration = v8.NamedPropertyHandlerConfiguration{
406+
.getter = struct {
407+
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
408+
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
409+
const _isolate = info.getIsolate();
410+
const v8_context = _isolate.getCurrentContext();
411+
412+
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
413+
414+
const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, _isolate, v8_context) catch "???";
415+
if (js_context.global_callback.?.missing(property, js_context)) {
416+
return v8.Intercepted.Yes;
417+
}
418+
return v8.Intercepted.No;
419+
}
420+
}.callback,
421+
.flags = v8.PropertyHandlerFlags.NonMasking | v8.PropertyHandlerFlags.OnlyInterceptStrings,
422+
};
423+
global_template.setNamedProperty(configuration, null);
424+
}
425+
402426
// All the FunctionTemplates that we created and setup in Env.init
403427
// are now going to get associated with our global instance.
404428
inline for (Types, 0..) |s, i| {
@@ -481,6 +505,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
481505
.ptr = safe_module_loader,
482506
.func = ModuleLoader.fetchModuleSource,
483507
},
508+
.global_callback = global_callback,
484509
};
485510

486511
var js_context = &self.js_context.?;
@@ -633,6 +658,9 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
633658
// necessary to lookup/store the dependent module in the module_cache.
634659
module_identifier: std.AutoHashMapUnmanaged(u32, []const u8) = .empty,
635660

661+
// Global callback is called on missing property.
662+
global_callback: ?GlobalMissingCallback = null,
663+
636664
const ModuleLoader = struct {
637665
ptr: *anyopaque,
638666
func: *const fn (ptr: *anyopaque, specifier: []const u8) anyerror!?[]const u8,
@@ -2265,11 +2293,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
22652293

22662294
fn generateNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
22672295
if (@hasDecl(Struct, "named_get") == false) {
2268-
if (comptime builtin.mode == .Debug) {
2269-
if (log.enabled(.unknown_prop, .debug)) {
2270-
generateDebugNamedIndexer(Struct, template_proto);
2271-
}
2272-
}
22732296
return;
22742297
}
22752298

@@ -2329,31 +2352,6 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
23292352
template_proto.setNamedProperty(configuration, null);
23302353
}
23312354

2332-
fn generateDebugNamedIndexer(comptime Struct: type, template_proto: v8.ObjectTemplate) void {
2333-
const configuration = v8.NamedPropertyHandlerConfiguration{
2334-
.getter = struct {
2335-
fn callback(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
2336-
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
2337-
const isolate = info.getIsolate();
2338-
const v8_context = isolate.getCurrentContext();
2339-
2340-
const js_context: *JsContext = @ptrFromInt(v8_context.getEmbedderData(1).castTo(v8.BigInt).getUint64());
2341-
2342-
const property = valueToString(js_context.call_arena, .{ .handle = c_name.? }, isolate, v8_context) catch "???";
2343-
log.debug(.unknown_prop, "unkown property", .{ .@"struct" = @typeName(Struct), .property = property });
2344-
return v8.Intercepted.No;
2345-
}
2346-
}.callback,
2347-
2348-
// This is really cool. Without this, we'd intercept _all_ properties
2349-
// even those explicitly set. So, node.length for example would get routed
2350-
// to our `named_get`, rather than a `get_length`. This might be
2351-
// useful if we run into a type that we can't model properly in Zig.
2352-
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
2353-
};
2354-
template_proto.setNamedProperty(configuration, null);
2355-
}
2356-
23572355
fn generateUndetectable(comptime Struct: type, template: v8.ObjectTemplate) void {
23582356
const has_js_call_as_function = @hasDecl(Struct, "jsCallAsFunction");
23592357

@@ -2577,6 +2575,35 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
25772575
self.callScopeEndFn(self.ptr);
25782576
}
25792577
};
2578+
2579+
// Callback called on global's property mssing.
2580+
// Return true to intercept the exectution or false to let the call
2581+
// continue the chain.
2582+
pub const GlobalMissingCallback = struct {
2583+
ptr: *anyopaque,
2584+
missingFn: *const fn (ptr: *anyopaque, name: []const u8, ctx: *JsContext) bool,
2585+
2586+
pub fn init(ptr: anytype) GlobalMissingCallback {
2587+
const T = @TypeOf(ptr);
2588+
const ptr_info = @typeInfo(T);
2589+
2590+
const gen = struct {
2591+
pub fn missing(pointer: *anyopaque, name: []const u8, ctx: *JsContext) bool {
2592+
const self: T = @ptrCast(@alignCast(pointer));
2593+
return ptr_info.pointer.child.missing(self, name, ctx);
2594+
}
2595+
};
2596+
2597+
return .{
2598+
.ptr = ptr,
2599+
.missingFn = gen.missing,
2600+
};
2601+
}
2602+
2603+
pub fn missing(self: GlobalMissingCallback, name: []const u8, ctx: *JsContext) bool {
2604+
return self.missingFn(self.ptr, name, ctx);
2605+
}
2606+
};
25802607
};
25812608
}
25822609

src/runtime/testing.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub fn Runner(comptime State: type, comptime Global: type, comptime types: anyty
5353
state,
5454
{},
5555
true,
56+
null,
5657
);
5758
return self;
5859
}

0 commit comments

Comments
 (0)