LibJS: Separate raw and capturing native functions

NativeFunction previously stored an AK::Function for every builtin,
even when the callable was just a plain C++ entry point. That mixed
together two different representations, made simple builtins carry
capture storage they did not need, and forced the GC to treat every
native function as if it might contain captured JS values.

Introduce RawNativeFunction for plain NativeFunctionPointer callees
and keep AK::Function-backed callables on a CapturingNativeFunction
subclass. Update the straightforward native registrations in LibJS
and LibWeb to use the raw representation, while leaving exported
Wasm functions on the capturing path because they still capture
state.

Wrap UniversalGlobalScope's byte-length strategy lambda in
Function<...> explicitly so it keeps selecting the capturing
NativeFunction::create overload.
This commit is contained in:
Andreas Kling 2026-04-14 21:45:44 +02:00 committed by Andreas Kling
parent fadea53343
commit 8a9d5ee1a1
9 changed files with 163 additions and 21 deletions

View file

@ -195,6 +195,7 @@ class ModuleEnvironment;
class Module;
struct ModuleRequest;
class NativeFunction;
class RawNativeFunction;
class NativeJavaScriptBackedFunction;
class ObjectEnvironment;
struct ParserError;
@ -311,6 +312,7 @@ struct TimeZoneOffset;
template<typename T>
requires(!IsLvalueReference<T>)
class ThrowCompletionOr;
using NativeFunctionPointer = ThrowCompletionOr<Value> (*)(VM&);
namespace Bytecode {

View file

@ -15,11 +15,50 @@
namespace JS {
GC_DEFINE_ALLOCATOR(NativeFunction);
GC_DEFINE_ALLOCATOR(RawNativeFunction);
namespace {
class CapturingNativeFunction final : public NativeFunction {
JS_OBJECT(CapturingNativeFunction, NativeFunction);
GC_DECLARE_ALLOCATOR(CapturingNativeFunction);
public:
CapturingNativeFunction(Function<ThrowCompletionOr<Value>(VM&)> native_function, Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin)
: NativeFunction(prototype, realm, builtin)
, m_native_function(move(native_function))
{
}
CapturingNativeFunction(Utf16FlyString name, Function<ThrowCompletionOr<Value>(VM&)> native_function, Object& prototype)
: NativeFunction(move(name), prototype)
, m_native_function(move(native_function))
{
}
virtual ThrowCompletionOr<Value> call() override
{
VERIFY(m_native_function);
return m_native_function(vm());
}
private:
virtual void visit_edges(Cell::Visitor& visitor) override
{
NativeFunction::visit_edges(visitor);
visitor.visit_possible_values(m_native_function.raw_capture_range());
}
AK::Function<ThrowCompletionOr<Value>(VM&)> m_native_function;
};
GC_DEFINE_ALLOCATOR(CapturingNativeFunction);
}
void NativeFunction::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit_possible_values(m_native_function.raw_capture_range());
visitor.visit(m_realm);
}
@ -44,7 +83,7 @@ GC::Ref<NativeFunction> NativeFunction::create(Realm& allocating_realm, Function
// 7. Set func.[[Extensible]] to true.
// 8. Set func.[[Realm]] to realm.
// 9. Set func.[[InitialName]] to null.
auto function = allocating_realm.create<NativeFunction>(move(behaviour), prototype, *realm.value(), builtin);
auto function = allocating_realm.create<CapturingNativeFunction>(move(behaviour), prototype, *realm.value(), builtin);
function->unsafe_set_shape(realm.value()->intrinsics().native_function_shape());
@ -61,14 +100,43 @@ GC::Ref<NativeFunction> NativeFunction::create(Realm& allocating_realm, Function
return function;
}
GC::Ref<NativeFunction> NativeFunction::create(Realm& realm, Utf16FlyString const& name, Function<ThrowCompletionOr<Value>(VM&)> function)
GC::Ref<NativeFunction> NativeFunction::create(Realm& allocating_realm, NativeFunctionPointer behaviour, i32 length, PropertyKey const& name, Optional<Realm*> realm, Optional<StringView> const& prefix, Optional<Bytecode::Builtin> builtin)
{
return realm.create<NativeFunction>(name, move(function), realm.intrinsics().function_prototype());
return RawNativeFunction::create(allocating_realm, behaviour, length, name, realm, prefix, builtin);
}
NativeFunction::NativeFunction(AK::Function<ThrowCompletionOr<Value>(VM&)> native_function, Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin)
GC::Ref<NativeFunction> NativeFunction::create(Realm& realm, Utf16FlyString const& name, Function<ThrowCompletionOr<Value>(VM&)> function)
{
return realm.create<CapturingNativeFunction>(name, move(function), realm.intrinsics().function_prototype());
}
GC::Ref<NativeFunction> NativeFunction::create(Realm& realm, Utf16FlyString const& name, NativeFunctionPointer function)
{
return RawNativeFunction::create(realm, name, function);
}
GC::Ref<RawNativeFunction> RawNativeFunction::create(Realm& allocating_realm, NativeFunctionPointer behaviour, i32 length, PropertyKey const& name, Optional<Realm*> realm, Optional<StringView> const& prefix, Optional<Bytecode::Builtin> builtin)
{
auto& vm = allocating_realm.vm();
if (!realm.has_value())
realm = vm.current_realm();
auto prototype = realm.value()->intrinsics().function_prototype();
auto function = allocating_realm.create<RawNativeFunction>(behaviour, prototype, *realm.value(), builtin);
function->unsafe_set_shape(realm.value()->intrinsics().native_function_shape());
function->put_direct(realm.value()->intrinsics().native_function_length_offset(), Value { length });
function->put_direct(realm.value()->intrinsics().native_function_name_offset(), function->make_function_name(name, prefix));
return function;
}
GC::Ref<RawNativeFunction> RawNativeFunction::create(Realm& realm, Utf16FlyString const& name, NativeFunctionPointer function)
{
return realm.create<RawNativeFunction>(name, function, realm.intrinsics().function_prototype());
}
NativeFunction::NativeFunction(Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin)
: FunctionObject(realm, prototype)
, m_native_function(move(native_function))
, m_realm(realm)
{
m_builtin = builtin;
@ -84,14 +152,6 @@ NativeFunction::NativeFunction(Object& prototype)
{
}
NativeFunction::NativeFunction(Utf16FlyString name, AK::Function<ThrowCompletionOr<Value>(VM&)> native_function, Object& prototype)
: FunctionObject(prototype)
, m_name(move(name))
, m_native_function(move(native_function))
, m_realm(prototype.shape().realm())
{
}
NativeFunction::NativeFunction(Utf16FlyString name, Object& prototype)
: FunctionObject(prototype)
, m_name(move(name))
@ -99,6 +159,18 @@ NativeFunction::NativeFunction(Utf16FlyString name, Object& prototype)
{
}
RawNativeFunction::RawNativeFunction(NativeFunctionPointer native_function, Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin)
: NativeFunction(prototype, realm, builtin)
, m_native_function(native_function)
{
}
RawNativeFunction::RawNativeFunction(Utf16FlyString name, NativeFunctionPointer native_function, Object& prototype)
: NativeFunction(move(name), prototype)
, m_native_function(native_function)
{
}
// NOTE: Do not attempt to DRY these, it's not worth it. The difference in return types (Value vs Object*),
// called functions (call() vs construct(FunctionObject&)), and this value (passed vs uninitialized) make
// these good candidates for a bit of code duplication :^)
@ -213,6 +285,11 @@ ThrowCompletionOr<GC::Ref<Object>> NativeFunction::internal_construct(ExecutionC
}
ThrowCompletionOr<Value> NativeFunction::call()
{
VERIFY_NOT_REACHED();
}
ThrowCompletionOr<Value> RawNativeFunction::call()
{
VERIFY(m_native_function);
return m_native_function(vm());

View file

@ -22,7 +22,9 @@ class JS_API NativeFunction : public FunctionObject {
public:
static GC::Ref<NativeFunction> create(Realm&, ESCAPING Function<ThrowCompletionOr<Value>(VM&)> behaviour, i32 length, PropertyKey const& name = Utf16FlyString {}, Optional<Realm*> = {}, Optional<StringView> const& prefix = {}, Optional<Bytecode::Builtin> builtin = {});
static GC::Ref<NativeFunction> create(Realm&, NativeFunctionPointer behaviour, i32 length, PropertyKey const& name = Utf16FlyString {}, Optional<Realm*> = {}, Optional<StringView> const& prefix = {}, Optional<Bytecode::Builtin> builtin = {});
static GC::Ref<NativeFunction> create(Realm&, Utf16FlyString const& name, ESCAPING Function<ThrowCompletionOr<Value>(VM&)>);
static GC::Ref<NativeFunction> create(Realm&, Utf16FlyString const& name, NativeFunctionPointer);
virtual ~NativeFunction() override = default;
@ -30,7 +32,6 @@ public:
virtual ThrowCompletionOr<GC::Ref<Object>> internal_construct(ExecutionContext&, FunctionObject& new_target) override;
// Used for [[Call]] / [[Construct]]'s "...result of evaluating F in a manner that conforms to the specification of F".
// Needs to be overridden by all NativeFunctions without an m_native_function.
virtual ThrowCompletionOr<Value> call();
virtual ThrowCompletionOr<GC::Ref<Object>> construct(FunctionObject& new_target);
@ -48,9 +49,8 @@ public:
virtual size_t function_environment_bindings_count() const { return 0; }
protected:
NativeFunction(Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin);
NativeFunction(Utf16FlyString name, Object& prototype);
NativeFunction(AK::Function<ThrowCompletionOr<Value>(VM&)>, Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin);
NativeFunction(Utf16FlyString name, AK::Function<ThrowCompletionOr<Value>(VM&)>, Object& prototype);
explicit NativeFunction(Object& prototype);
virtual void visit_edges(Cell::Visitor& visitor) override;
@ -60,11 +60,36 @@ private:
Utf16FlyString m_name;
Optional<Utf16FlyString> m_initial_name; // [[InitialName]]
AK::Function<ThrowCompletionOr<Value>(VM&)> m_native_function;
GC::Ref<Realm> m_realm;
};
template<>
inline bool Object::fast_is<NativeFunction>() const { return is_native_function(); }
class JS_API RawNativeFunction final : public NativeFunction {
JS_OBJECT(RawNativeFunction, NativeFunction);
GC_DECLARE_ALLOCATOR(RawNativeFunction);
public:
static GC::Ref<RawNativeFunction> create(Realm&, NativeFunctionPointer behaviour, i32 length, PropertyKey const& name = Utf16FlyString {}, Optional<Realm*> = {}, Optional<StringView> const& prefix = {}, Optional<Bytecode::Builtin> builtin = {});
static GC::Ref<RawNativeFunction> create(Realm&, Utf16FlyString const& name, NativeFunctionPointer);
virtual ~RawNativeFunction() override = default;
virtual ThrowCompletionOr<Value> call() override;
NativeFunctionPointer native_function() const { return m_native_function; }
private:
RawNativeFunction(NativeFunctionPointer, Object* prototype, Realm& realm, Optional<Bytecode::Builtin> builtin);
RawNativeFunction(Utf16FlyString name, NativeFunctionPointer, Object& prototype);
virtual bool is_raw_native_function() const final { return true; }
NativeFunctionPointer m_native_function { nullptr };
};
template<>
inline bool Object::fast_is<RawNativeFunction>() const { return is_raw_native_function(); }
}

View file

@ -1404,6 +1404,17 @@ void Object::set_prototype(Object* new_prototype)
m_shape = shape().create_prototype_transition(new_prototype);
}
void Object::define_native_accessor(Realm& realm, PropertyKey const& property_key, NativeFunctionPointer getter, NativeFunctionPointer setter, PropertyAttributes attribute)
{
FunctionObject* getter_function = nullptr;
if (getter)
getter_function = NativeFunction::create(realm, move(getter), 0, property_key, &realm, "get"sv);
FunctionObject* setter_function = nullptr;
if (setter)
setter_function = NativeFunction::create(realm, move(setter), 1, property_key, &realm, "set"sv);
define_direct_accessor(property_key, getter_function, setter_function, attribute);
}
void Object::define_native_accessor(Realm& realm, PropertyKey const& property_key, Function<ThrowCompletionOr<Value>(VM&)> getter, Function<ThrowCompletionOr<Value>(VM&)> setter, PropertyAttributes attribute)
{
FunctionObject* getter_function = nullptr;
@ -1510,6 +1521,12 @@ Value Object::get_without_side_effects(PropertyKey const& property_key) const
return {};
}
void Object::define_native_function(Realm& realm, PropertyKey const& property_key, NativeFunctionPointer native_function, i32 length, PropertyAttributes attribute, Optional<Bytecode::Builtin> builtin)
{
auto function = NativeFunction::create(realm, move(native_function), length, property_key, &realm, {}, builtin);
define_direct_property(property_key, function, attribute);
}
void Object::define_native_function(Realm& realm, PropertyKey const& property_key, Function<ThrowCompletionOr<Value>(VM&)> native_function, i32 length, PropertyAttributes attribute, Optional<Bytecode::Builtin> builtin)
{
auto function = NativeFunction::create(realm, move(native_function), length, property_key, &realm, {}, builtin);

View file

@ -225,7 +225,9 @@ public:
using IntrinsicAccessor = Value (*)(Realm&);
void define_intrinsic_accessor(PropertyKey const&, PropertyAttributes attributes, IntrinsicAccessor accessor);
void define_native_function(Realm&, PropertyKey const&, NativeFunctionPointer, i32 length, PropertyAttributes attributes, Optional<Bytecode::Builtin> builtin = {});
void define_native_function(Realm&, PropertyKey const&, ESCAPING Function<ThrowCompletionOr<Value>(VM&)>, i32 length, PropertyAttributes attributes, Optional<Bytecode::Builtin> builtin = {});
void define_native_accessor(Realm&, PropertyKey const&, NativeFunctionPointer getter, NativeFunctionPointer setter, PropertyAttributes attributes);
void define_native_accessor(Realm&, PropertyKey const&, ESCAPING Function<ThrowCompletionOr<Value>(VM&)> getter, ESCAPING Function<ThrowCompletionOr<Value>(VM&)> setter, PropertyAttributes attributes);
void define_native_javascript_backed_function(PropertyKey const&, GC::Ref<NativeJavaScriptBackedFunction> function, i32 length, PropertyAttributes attributes);
@ -254,6 +256,7 @@ public:
virtual bool is_global_object() const { return false; }
virtual bool is_proxy_object() const { return false; }
virtual bool is_native_function() const { return false; }
virtual bool is_raw_native_function() const { return false; }
[[nodiscard]] bool is_ecmascript_function_object() const { return m_flags & Flag::IsECMAScriptFunctionObject; }
void set_is_ecmascript_function_object() { m_flags |= Flag::IsECMAScriptFunctionObject; }
void set_is_function() { m_flags |= Flag::IsFunction; }

View file

@ -144,7 +144,7 @@ GC::Ref<WebIDL::CallbackType> UniversalGlobalScopeMixin::byte_length_queuing_str
};
// 2. Let F be ! CreateBuiltinFunction(steps, 1, "size", « », globalObjects relevant Realm).
auto function = JS::NativeFunction::create(realm, move(steps), 1, "size"_utf16_fly_string, &realm);
auto function = JS::NativeFunction::create(realm, Function<JS::ThrowCompletionOr<JS::Value>(JS::VM&)> { move(steps) }, 1, "size"_utf16_fly_string, &realm);
// 3. Set globalObjects byte length queuing strategy size function to a Function that represents a reference to F, with callback context equal to globalObjects relevant settings object.
// FIXME: Update spec comment to pass globalObject's relevant realm once Streams spec is updated for ShadowRealm spec

View file

@ -575,11 +575,24 @@ GC::Ref<ExportedWasmFunction> ExportedWasmFunction::create(JS::Realm& realm, Utf
}
ExportedWasmFunction::ExportedWasmFunction(Utf16FlyString name, AK::Function<JS::ThrowCompletionOr<JS::Value>(JS::VM&)> behavior, Wasm::FunctionAddress exported_address, JS::Object& prototype)
: NativeFunction(move(name), move(behavior), prototype)
: NativeFunction(move(name), prototype)
, m_behavior(move(behavior))
, m_exported_address(exported_address)
{
}
void ExportedWasmFunction::visit_edges(Cell::Visitor& visitor)
{
NativeFunction::visit_edges(visitor);
visitor.visit_possible_values(m_behavior.raw_capture_range());
}
JS::ThrowCompletionOr<JS::Value> ExportedWasmFunction::call()
{
VERIFY(m_behavior);
return m_behavior(vm());
}
JS::NativeFunction* create_native_function(JS::VM& vm, Wasm::FunctionAddress address, Utf16FlyString name, Instance* instance)
{
auto& realm = *vm.current_realm();

View file

@ -94,10 +94,15 @@ public:
Wasm::FunctionAddress exported_address() const { return m_exported_address; }
virtual JS::ThrowCompletionOr<JS::Value> call() override;
protected:
ExportedWasmFunction(Utf16FlyString name, AK::Function<JS::ThrowCompletionOr<JS::Value>(JS::VM&)>, Wasm::FunctionAddress, Object& prototype);
private:
virtual void visit_edges(Cell::Visitor&) override;
AK::Function<JS::ThrowCompletionOr<JS::Value>(JS::VM&)> m_behavior;
Wasm::FunctionAddress m_exported_address;
};

View file

@ -34,5 +34,5 @@ test("constructing object", () => {
test("constructing native function", () => {
expect(() => {
new isNaN();
}).toThrowWithMessage(TypeError, "[object NativeFunction] is not a constructor (evaluated from 'isNaN')");
}).toThrowWithMessage(TypeError, "[object RawNativeFunction] is not a constructor (evaluated from 'isNaN')");
});