// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once // INTERNAL IMPLEMENTATION FILE // // Handles wrapping a C++ function so that it can be called from JavaScript, and vice versa. #include "jsg.h" #include "wrappable.h" #include #include #include #include namespace workerd::jsg { template class WrappableFunction; class WrappableFunctionBase: public Wrappable { public: kj::StringPtr jsgGetMemoryName() const override { return "WrappableFunction"_kjc; } void jsgGetMemoryInfo(MemoryTracker& tracker) const override { Wrappable::jsgGetMemoryInfo(tracker); visitForMemoryInfo(tracker); } virtual void visitForMemoryInfo(MemoryTracker& tracker) const { // TODO(soon): Implement tracking for WrappableFunction. } }; template class WrappableFunction: public WrappableFunctionBase { public: WrappableFunction(bool needsGcTracing): needsGcTracing(needsGcTracing) {} virtual Ret operator()(Lock& js, Args&&... args) = 0; const bool needsGcTracing; size_t jsgGetMemorySelfSize() const override { return sizeof(WrappableFunction); } }; template class WrappableFunctionImpl; template class WrappableFunctionImpl: public WrappableFunction { public: WrappableFunctionImpl(Impl&& func) : WrappableFunction(hasPublicVisitForGc()), func(kj::fwd(func)) {} Ret operator()(Lock& js, Args&&... args) override { return func(js, kj::fwd(args)...); } void jsgVisitForGc(GcVisitor& visitor) override { if constexpr (hasPublicVisitForGc()) { visitor.visit(func); } } private: Impl func; }; template > struct FunctorCallback; template struct FunctorCallback> { static void callback(const v8::FunctionCallbackInfo& args) { liftKj(args, [&]() { auto isolate = args.GetIsolate(); auto context = isolate->GetCurrentContext(); auto& js = Lock::from(isolate); auto& wrapper = TypeWrapper::from(isolate); auto& func = extractInternalPointer, false>( context, args.Data().As()); if constexpr (isVoid()) { func(Lock::from(isolate), wrapper.template unwrap( js, context, args, indexes, TypeErrorContext::callbackArgument(indexes))...); } else { return wrapper.wrap(js, context, args.This(), func(Lock::from(isolate), wrapper.template unwrap( js, context, args, indexes, TypeErrorContext::callbackArgument(indexes))...)); } }); } }; // Specialization for functions that take `const v8::FunctionCallbackInfo&` as their // second parameter (after Lock&). template struct FunctorCallback&, Args...), kj::_::Indexes> { static void callback(const v8::FunctionCallbackInfo& args) { liftKj(args, [&]() { auto isolate = args.GetIsolate(); auto context = isolate->GetCurrentContext(); auto& wrapper = TypeWrapper::from(isolate); auto& js = Lock::from(isolate); auto& func = extractInternalPointer< WrappableFunction&, Args...)>, false>( context, args.Data().As()); if constexpr (isVoid()) { func(js, args, wrapper.template unwrap( js, context, args, indexes, TypeErrorContext::callbackArgument(indexes))...); } else { return wrapper.wrap(js, context, args.This(), func(js, args, wrapper.template unwrap( js, context, args, indexes, TypeErrorContext::callbackArgument(indexes))...)); } }); } }; template class Function { // (Docs are in jsg.h, the public header.) public: using NativeFunction = jsg::WrappableFunction; // When holding a JavaScript function, `Wrapper` is a C++ function that will handle converting // C++ arguments into JavaScript values and then call the JS function. using Wrapper = Ret(jsg::Lock& js, v8::Local receiver, // the `this` value in the function v8::Local fn, Args...); Function(Wrapper* wrapper, V8Ref receiver, V8Ref function) : Function(wrapper, receiver.cast(jsg::Lock::current()), kj::mv(function)) {} // Construct jsg::Function wrapping a JavaScript function. Function(Wrapper* wrapper, Value receiver, V8Ref function) : impl(JsImpl{ .wrapper = kj::mv(wrapper), .receiver = kj::mv(receiver), .handle = kj::mv(function)}) {} // Construct jsg::Function wrapping a C++ function. The parameter can be a lambda or anything // else with operator() with a compatible signature. If the parameter has a visitForGc(GcVisitor&) // method, then GC visitation will be arranged. template ()(kj::instance(), kj::instance()...))> Function(Func&& func) : impl(Ref( alloc>(kj::fwd(func)))) {} Function(Function&&) = default; Function& operator=(Function&&) = default; KJ_DISALLOW_COPY(Function); Ret operator()(jsg::Lock& jsl, Args... args) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { return (*native)(jsl, kj::fwd(args)...); } KJ_CASE_ONEOF(js, JsImpl) { return (*js.wrapper)( jsl, js.receiver.getHandle(jsl), js.handle.getHandle(jsl), kj::fwd(args)...); } } __builtin_unreachable(); } // Get a handle to the underlying function. If this is a native function, // `makeNativeWrapper(Ref&)` is called to create the wrapper. // // Only the `FunctionWrapper` TypeWrapper mixin should call this. Anyone else needs to call // `tryGetHandle()`. template v8::Local getOrCreateHandle( v8::Isolate* isolate, MakeNativeWrapperFunc&& makeNativeWrapper) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { return makeNativeWrapper(native); } KJ_CASE_ONEOF(js, JsImpl) { return js.handle.getHandle(isolate); } } __builtin_unreachable(); } // Like getHandle() but if there's no wrapper yet, returns null. kj::Maybe> tryGetHandle(v8::Isolate* isolate) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { return kj::none; } KJ_CASE_ONEOF(js, JsImpl) { return js.handle.getHandle(isolate); } } __builtin_unreachable(); } inline void visitForGc(GcVisitor& visitor) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { visitor.visit(native); } KJ_CASE_ONEOF(js, JsImpl) { visitor.visit(js.receiver, js.handle); } } } inline Function addRef(v8::Isolate* isolate) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { return Function(native.addRef()); } KJ_CASE_ONEOF(js, JsImpl) { return Function( js.wrapper, js.receiver.addRef(isolate), js.handle.addRef(isolate)); } } __builtin_unreachable(); } inline Function addRef(Lock& js) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(native, Ref) { return Function(native.addRef()); } KJ_CASE_ONEOF(jsi, JsImpl) { return Function(jsi.wrapper, jsi.receiver.addRef(js), jsi.handle.addRef(js)); } } __builtin_unreachable(); } inline void setReceiver(Value receiver) { KJ_IF_SOME(i, impl.template tryGet()) { i.receiver = kj::mv(receiver); } } JSG_MEMORY_INFO(Function) { KJ_SWITCH_ONEOF(impl) { KJ_CASE_ONEOF(ref, Ref) { tracker.trackField("native", ref); } KJ_CASE_ONEOF(impl, JsImpl) { tracker.trackField("impl", impl); } } } private: Function(Ref&& func): impl(kj::mv(func)) {} struct JsImpl { Wrapper* wrapper; Value receiver; V8Ref handle; JSG_MEMORY_INFO(JsImpl) { tracker.trackField("receiver", receiver); tracker.trackField("handle", handle); } }; kj::OneOf, JsImpl> impl; friend class MemoryTracker; }; template class Constructor: public jsg::Function { public: using jsg::Function::Function; }; template struct MethodSignature_; template struct MethodSignature_ { using Type = Ret(Args...); }; template struct MethodSignature_ { using Type = Ret(Args...); }; // Extracts a function signature from a method type. template using MethodSignature = MethodSignature_::Type; // TypeWrapper mixin for functions / lambdas. template class FunctionWrapper { public: template ::operator())> static constexpr const char* getName(Func*) { return "function"; } template ::operator())>> v8::Local wrap(Lock& js, v8::Local context, kj::Maybe> creator, Func&& func) { return wrap(js, context, creator, jsg::Function(kj::mv(func))); } template v8::Local wrap(Lock& js, v8::Local context, kj::Maybe> creator, Function&& func) { v8::Isolate* isolate = js.v8Isolate; return func.getOrCreateHandle(isolate, [&](Ref>& ref) { v8::Local data; KJ_IF_SOME(h, ref->tryGetHandle(isolate)) { // Apparently, this function has been wrapped before and already has an opaque handle. // That's interesting. However, unfortunately, we don't have a handle to the v8::Function // that was created last time, so we can't return the same function instance. This is // arguably incorrect; what if the application added properties to it or something? // // Unfortunately, it is exceedingly difficult for us to store the function handle for // reuse without introducing performance problems. // - Ideally, we'd use the v8::Function itself as the object's wrapper, rather than an // "opaque" wrapper. However, this doesn't work, because we can't attach internal fields // to it. v8::Function::New() does not let us specify an internal field count. We can // specify internal fields if we create a FunctionTemplate and then create the Function // from that, but a FunctionTemplate only instantiates one Function (per Context). We // need a separate Function instance for object we want to wrap. So... this doesn't work. // (Note that V8's heap tracing API deeply depends on wrapper objects having two internal // fields, so using other schemes like v8::External doesn't help either.) // - Another approach might be to store the v8::Function on the WrappableFunction, once // it's created. This is a cyclic reference, but we could rely on GC visitation to // collect it. The problem is, cyclic references can only be collected by tracing, not // by scavenging. Tracing runs much less often than scavenging. So we'd be forcing every // function object to live on the heap longer than otherwise necessary. // // In practice, it probably never matters that returning the same jsg::Function twice // produces exactly the same JavaScript handle. So... screw it. data = h; } else { data = ref->attachOpaqueWrapper(context, ref->needsGcTracing); } // TODO(conform): Correctly set `length` on all functions. Probably doesn't need a compat flag // but I'd like to do it as a separate commit which can be reverted. We also currently fail // to set this on constructors and methods (see resource.h). Remember not to count // injected parameters! return check( v8::Function::New(context, &FunctorCallback::callback, data)); }); } template kj::Maybe> tryUnwrap(Lock& js, v8::Local context, v8::Local handle, Constructor*, kj::Maybe> parentObject) { if (!handle->IsFunction()) { return kj::none; } auto isolate = js.v8Isolate; auto wrapperFn = [](Lock& js, v8::Local receiver, v8::Local func, Args... args) -> Ret { auto isolate = js.v8Isolate; auto& typeWrapper = TypeWrapper::from(isolate); return js.withinHandleScope([&] { auto context = js.v8Context(); v8::Local argv[sizeof...(Args)]{ typeWrapper.wrap(js, context, kj::none, kj::fwd(args))...}; v8::Local result = check(func->NewInstance(context, sizeof...(Args), argv)); return typeWrapper.template unwrap( js, context, result, TypeErrorContext::callbackReturn()); }); }; return Constructor(wrapperFn, V8Ref(isolate, parentObject.orDefault(context->Global())), V8Ref(isolate, handle.As())); } template kj::Maybe> tryUnwrap(Lock& js, v8::Local context, v8::Local handle, Function*, kj::Maybe> parentObject) { if (!handle->IsFunction()) { return kj::none; } auto isolate = js.v8Isolate; auto wrapperFn = [](Lock& js, v8::Local receiver, v8::Local func, Args... args) -> Ret { auto isolate = js.v8Isolate; auto& typeWrapper = TypeWrapper::from(isolate); return js.withinHandleScope([&] { auto context = js.v8Context(); v8::LocalVector argv(js.v8Isolate, std::initializer_list>{ typeWrapper.wrap(js, context, kj::none, kj::fwd(args)) .template As()...}); auto result = check(func->Call(context, receiver, argv.size(), argv.data())); if constexpr (!isVoid()) { return typeWrapper.template unwrap( js, context, result, TypeErrorContext::callbackReturn()); } else { return; } }); }; return Function(wrapperFn, V8Ref(isolate, parentObject.orDefault(context->Global())), V8Ref(isolate, handle.As())); } template kj::Maybe)>> tryUnwrap(Lock& js, v8::Local context, v8::Local handle, Function)>*, kj::Maybe> parentObject) { if (!handle->IsFunction()) { return kj::none; } auto isolate = js.v8Isolate; auto wrapperFn = [](Lock& js, v8::Local receiver, v8::Local func, Arguments args) -> Ret { auto isolate = js.v8Isolate; auto& typeWrapper = TypeWrapper::from(isolate); return js.withinHandleScope([&] { auto context = js.v8Context(); v8::Local result; if (args.size() > 0) { v8::LocalVector argv(js.v8Isolate, args.size()); for (size_t n = 0; n < args.size(); n++) { argv[n] = args[n].getHandle(js); } result = check(func->Call(context, receiver, argv.size(), argv.data())); } else { result = check(func->Call(context, receiver, 0, nullptr)); } if constexpr (!isVoid()) { return typeWrapper.template unwrap( js, context, result, TypeErrorContext::callbackReturn()); } else { return; } }); }; return Function)>(wrapperFn, V8Ref(isolate, parentObject.orDefault(context->Global())), V8Ref(isolate, handle.As())); } }; template class VisitableLambda { public: VisitableLambda(Func&& func): func(kj::fwd(func)) {} template auto operator()(Params&&... params) { return func(kj::fwd(params)...); } void visitForGc(GcVisitor& visitor) { func(visitor); } private: Func func; }; template constexpr bool isGcVisitor() { return false; } template <> constexpr bool isGcVisitor() { return true; } #define JSG_VISITABLE_LAMBDA(CAPTURES, VISITS, ...) \ ::workerd::jsg::VisitableLambda([JSG_EXPAND CAPTURES](auto&&... params) mutable { \ if constexpr (::workerd::jsg::isGcVisitor()) { \ (params.visit VISITS, ...); \ } else { \ return ([&] __VA_ARGS__)(kj::fwd(params)...); \ } \ }) } // namespace workerd::jsg