current commit
261104.12 · ffi: support function calls from pointers
#if HAVE_FFI

#include "node_ffi.h"
#include <climits>
#include <cstring>
#include <limits>
#include <memory>
#include <optional>
#include "base_object-inl.h"
#include "env-inl.h"
#include "ffi/data.h"
#include "ffi/fast.h"
#include "ffi/jit_memory.h"
#include "ffi/types.h"
#include "node_binding.h"
#include "node_errors.h"
#include "node_internals.h"

namespace node {

using v8::Array;
using v8::ArrayBuffer;
using v8::BigInt;
using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::DontEnum;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::HandleScope;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::LocalVector;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Object;
using v8::ObjectTemplate;
using v8::PropertyAttribute;
using v8::ReadOnly;
using v8::Signature;
using v8::String;
using v8::TryCatch;
using v8::Value;

namespace ffi {

void FFIFunction::Invoke(void* result, void** values) {
#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
  if (call_plan != nullptr) {
    ffi_call_plan_invoke(call_plan.get(), FFI_FN(ptr), result, values);
    return;
  }
#endif

  ffi_call(&cif, FFI_FN(ptr), result, values);
}

void FFIFunctionInfo::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("sb_backing", sb_backing);
}

DynamicLibrary::DynamicLibrary(Environment* env, Local<Object> object)
    : BaseObject(env, object) {
  MakeWeak();
  env->AddCleanupHook(CleanupHook, this);
}

DynamicLibrary::~DynamicLibrary() {
  env()->RemoveCleanupHook(CleanupHook, this);
  this->Close();
}

void DynamicLibrary::CleanupHook(void* arg) {
  DynamicLibrary* lib = static_cast<DynamicLibrary*>(arg);
  // Registered callbacks hold strong references to the library, which would
  // otherwise keep it alive past Environment teardown. Release them only
  // after Close() returns, since dropping the last reference may delete lib.
  decltype(lib->callbacks_) callbacks;
  callbacks.swap(lib->callbacks_);
  lib->Close();
}

bool DynamicLibrary::is_closed() const {
  return static_cast<void*>(lib_.handle) == nullptr;
}

void DynamicLibrary::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackFieldWithSize("path", path_.capacity() + 1, "std::string");

  size_t symbols_size = 0;
  for (const auto& [name, ptr] : symbols_) {
    symbols_size += name.capacity() + 1;
    symbols_size += sizeof(ptr);
    symbols_size += sizeof(decltype(symbols_)::value_type);
  }

  tracker->TrackFieldWithSize(
      "symbols", symbols_size, "std::unordered_map<std::string, void*>");

  tracker->TrackFieldWithSize(
      "function_wrappers",
      function_wrappers_.size() *
          sizeof(decltype(function_wrappers_)::value_type),
      "std::unordered_map<std::string, v8::Global<v8::Function>>");

  tracker->TrackFieldWithSize(
      "pointer_functions",
      pointer_functions_ == nullptr
          ? 0
          : sizeof(std::unordered_set<FFIFunction*>) +
                pointer_functions_->size() * sizeof(FFIFunction*),
      "std::unordered_set<FFIFunction*>");

  // FFIFunctionInfo instances and their sb_backing ArrayBuffers are
  // owned by V8 function wrappers and reachable only via weak references,
  // so they are deliberately not counted here.
}

void DynamicLibrary::Close() {
  for (auto& [name, fn] : functions_) {
    fn->closed = true;
    fn->ptr = nullptr;
  }

  if (pointer_functions_ != nullptr) {
    for (FFIFunction* fn : *pointer_functions_) {
      fn->closed = true;
      fn->ptr = nullptr;
    }
    pointer_functions_->clear();
  }

  // Closing the library invalidates all registered callbacks. Node.js does not
  // track or revoke callback pointers that have already been handed to native
  // code. If native code calls a callback pointer after `close()` or
  // `unregisterCallback()`, the behavior is undefined, not allowed, and
  // dangerous: it can crash the process, produce incorrect output, or corrupt
  // memory.

  if (!is_closed()) {
    uv_dlclose(&lib_);
    lib_ = {};
  }

  symbols_.clear();
  functions_.clear();
  function_wrappers_.clear();
  callbacks_.clear();
}

Maybe<void*> DynamicLibrary::ResolveSymbol(Environment* env,
                                           const std::string& name) {
  if (is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return {};
  }

  auto existing = symbols_.find(name);
  void* ptr;

  if (existing != symbols_.end()) {
    ptr = existing->second;
  } else {
    if (uv_dlsym(&lib_, name.c_str(), &ptr) != 0) {
      THROW_ERR_FFI_CALL_FAILED(env, "dlsym failed: %s", uv_dlerror(&lib_));
      return {};
    }
  }

  return Just(ptr);
}

Maybe<DynamicLibrary::PreparedFunction> DynamicLibrary::PrepareFunction(
    Environment* env,
    const std::string& name,
    Local<Object> signature,
    void* ptr) {
  std::shared_ptr<FFIFunction> fn;
  FunctionSignature parsed;

  if (!ParseFunctionSignature(env, name, signature).To(&parsed)) {
    return {};
  }
  // Look up the cache only after parsing: the signature's getters run user
  // code that may close the library, which clears `functions_`.
  const bool from_pointer = ptr != nullptr;
  auto existing = from_pointer ? functions_.end() : functions_.find(name);
  auto [return_type, args, return_type_name, arg_type_names] =
      std::move(parsed);

  bool should_cache_symbol = false;
  bool should_cache_function = false;

  if (existing == functions_.end()) {
    if (!from_pointer) {
      if (!ResolveSymbol(env, name).To(&ptr)) {
        return {};
      }
      should_cache_symbol = symbols_.find(name) == symbols_.end();
    }

    fn = std::make_shared<FFIFunction>();
    fn->ptr = ptr;
    fn->args = std::move(args);
    fn->return_type = return_type;
    fn->arg_type_names = std::move(arg_type_names);
    fn->return_type_name = std::move(return_type_name);

    ffi_status status = ffi_prep_cif(&fn->cif,
                                     FFI_DEFAULT_ABI,
                                     fn->args.size(),
                                     fn->return_type,
                                     fn->args.data());
    if (status != FFI_OK) {
      const char* msg = "ffi_prep_cif failed";
      switch (status) {
        case FFI_BAD_TYPEDEF:
          msg = "ffi_prep_cif failed: bad typedef";
          break;
        case FFI_BAD_ABI:
          msg = "ffi_prep_cif failed: bad ABI";
          break;
        default:
          msg = "ffi_prep_cif failed: unknown error";
          break;
      }

      THROW_ERR_FFI_CALL_FAILED(env, msg);
      return {};
    }

#if defined(NODE_FFI_HAS_FAST_CALL_PLAN)
    // Allocation failure is non-fatal. Invoke() falls back to ffi_call().
    ffi_call_plan* call_plan = ffi_call_plan_alloc(&fn->cif);
    if (call_plan != nullptr) {
      fn->call_plan.reset(call_plan);
    }
#endif

    should_cache_function = !from_pointer;
  } else {
    fn = existing->second;

    if (!SignaturesMatch(*fn, return_type, args)) {
      THROW_ERR_INVALID_ARG_VALUE(
          env,
          "Function %s"
          " was already requested with a different signature",
          name);
      return {};
    }
  }

  return Just(PreparedFunction{fn, should_cache_symbol, should_cache_function});
}

FFIFunctionInfo::FFIFunctionInfo(Environment* env,
                                 Local<Object> object,
                                 std::shared_ptr<FFIFunction> fn,
                                 DynamicLibrary* library)
    : BaseObject(env, object), fn(std::move(fn)) {
  // Keep the DynamicLibrary instance alive as long as any of its functions are
  // alive
  object->SetInternalField(FFIFunctionInfo::kLibrary, library->object());
}

Local<FunctionTemplate> FFIFunctionInfo::GetConstructorTemplate(
    IsolateData* isolate_data) {
  Local<FunctionTemplate> tmpl =
      isolate_data->ffi_function_constructor_template();
  if (tmpl.IsEmpty()) {
    Isolate* isolate = isolate_data->isolate();
    tmpl = MakeLazilyInitializedJSTemplate(isolate_data, kInternalFieldCount);
    Local<String> classname = FIXED_ONE_BYTE_STRING(isolate, "FFIFunctionInfo");
    tmpl->SetClassName(classname);
    auto instance = tmpl->InstanceTemplate();
    instance->SetInternalFieldCount(FFIFunctionInfo::kInternalFieldCount);
    isolate_data->set_ffi_function_constructor_template(tmpl);
  }
  return tmpl;
}

BaseObjectPtr<FFIFunctionInfo> FFIFunctionInfo::Create(
    Environment* env,
    std::shared_ptr<FFIFunction> fn,
    DynamicLibrary* library) {
  Local<Object> obj;
  if (!GetConstructorTemplate(env->isolate_data())
           ->InstanceTemplate()
           ->NewInstance(env->context())
           .ToLocal(&obj)) {
    return nullptr;
  }
  return MakeWeakBaseObject<FFIFunctionInfo>(env, obj, std::move(fn), library);
}

// V8 strongly caches instantiations of low-serial FunctionTemplates, which
// would root the function and, through FFIFunctionInfo, its library and
// trampoline. An ObjectTemplate property is the only public way to make a
// FunctionTemplate uncacheable (see v8::Template::Set()), so add one and delete
// it from the instance. It is shared because its own instantiation is cached.
static MaybeLocal<Function> GetUncachedFunction(Environment* env,
                                                Local<FunctionTemplate> tmpl) {
  Local<Context> context = env->context();
  IsolateData* isolate_data = env->isolate_data();

  Local<ObjectTemplate> placeholder =
      isolate_data->ffi_uncacheable_placeholder_template();
  if (placeholder.IsEmpty()) {
    placeholder = ObjectTemplate::New(env->isolate());
    isolate_data->set_ffi_uncacheable_placeholder_template(placeholder);
  }
  tmpl->Set(env->pointer_string(), placeholder);

  Local<Function> ret;
  if (!tmpl->GetFunction(context).ToLocal(&ret) ||
      !ret->Delete(context, env->pointer_string()).FromMaybe(false)) {
    return MaybeLocal<Function>();
  }
  return ret;
}

MaybeLocal<Function> DynamicLibrary::CreateFunction(
    Environment* env,
    const std::string& name,
    const std::shared_ptr<FFIFunction>& fn) {
  Isolate* isolate = env->isolate();

  // Creating a callable emits a trampoline, allocates an FFIFunctionInfo, and
  // on the SharedBuffer path allocates an ArrayBuffer, so reuse the one already
  // handed out for this symbol. `PrepareFunction()` rejects a request that uses
  // a different signature, so a hit always describes the same signature. An
  // empty handle means the wrapper was collected; fall through and rebuild.
  auto cached = function_wrappers_.find(name);
  if (cached != function_wrappers_.end()) {
    if (!cached->second.IsEmpty()) {
      return cached->second.Get(isolate);
    }
    function_wrappers_.erase(cached);
  }

  Local<Function> ret;
  if (!BuildFunction(env, name, fn, true).ToLocal(&ret)) {
    return {};
  }
  function_wrappers_.emplace(name, Global<Function>(isolate, ret))
      .first->second.SetWeak();
  return ret;
}

MaybeLocal<Function> DynamicLibrary::BuildFunction(
    Environment* env,
    const std::string& name,
    const std::shared_ptr<FFIFunction>& fn,
    bool optimize) {
  Isolate* isolate = env->isolate();
  Local<Context> context = env->context();
  auto info = FFIFunctionInfo::Create(env, fn, this);
  if (!info) {
    return {};
  }

  DCHECK_EQ(fn->args.size(), fn->arg_type_names.size());

  // Try the generated Fast API path first. If metadata creation rejects the
  // signature, fall back to SharedBuffer for supported scalar shapes, then to
  // the generic libffi invoker.
  if (optimize) {
    std::shared_ptr<FFIFunction> fast_fn = CloneWithRawPointerArgNames(fn);
    if (!CreateFastFFIMetadata(*fast_fn, &fn->closed, isolate)
             .MoveTo(&info->fast_metadata)) {
      return {};
    }
  }
  bool use_fast_api = info->fast_metadata != nullptr;
  bool use_sb = optimize && !use_fast_api && IsSBEligibleSignature(*fn);
  bool has_ptr_args = use_sb && SignatureHasPointerArgs(*fn);
  // Signatures that need JS-side conversion or validation use a wrapper, as
  // do all fast signatures on platforms without a native library guard.
  // Pointer arguments need the wrapper's range check because V8 truncates
  // BigInts passed to Fast API uint64 parameters.
  bool needs_fast_argument_wrapper =
      use_fast_api &&
      (SignatureNeedsRawPointerConversions(*fn) ||
       SignatureNeedsFastIntegerValidation(*fn) ||
       SignatureHasPointerArgs(*fn) || !info->fast_metadata->guards_library);
  // A single pointer-like parameter can get a separate Buffer-aware Fast API
  // entrypoint so Buffer calls avoid JS pointer extraction.
  bool needs_fast_buffer_invoke =
      use_fast_api && SignatureNeedsFastBufferInvoke(*fn);

  MaybeLocal<Function> maybe_ret;
  if (use_fast_api) {
    // V8 calls this FunctionTemplate through `fast_metadata->c_function` when
    // the optimized Fast API call path is available. The normal callback stays
    // attached as a fallback for V8 deopts and unsupported call sites.
    Local<FunctionTemplate> tmpl =
        FunctionTemplate::New(isolate,
                              DynamicLibrary::InvokeFunction,
                              info->object(),
                              Local<v8::Signature>(),
                              fn->args.size(),
                              v8::ConstructorBehavior::kThrow,
                              v8::SideEffectType::kHasSideEffect,
                              &info->fast_metadata->c_function);
    maybe_ret = GetUncachedFunction(env, tmpl);
  } else {
    // Non-Fast signatures either use the SharedBuffer invoker, where JS writes
    // argument slots before calling with no arguments, or the generic invoker
    // that converts each JS argument in C++.
    maybe_ret = Function::New(context,
                              use_sb ? DynamicLibrary::InvokeFunctionSB
                                     : DynamicLibrary::InvokeFunction,
                              info->object(),
                              0,
                              v8::ConstructorBehavior::kThrow);
  }

  Local<Function> ret;
  if (!maybe_ret.ToLocal(&ret)) {
    return MaybeLocal<Function>();
  }

  Local<Value> name_str;
  if (!ToV8Value(env->context(), name, isolate).ToLocal(&name_str)) {
    return MaybeLocal<Function>();
  }
  ret->SetName(name_str.As<String>());

  if (!ret->CreateDataProperty(
              context,
              env->pointer_string(),
              BigInt::NewFromUnsigned(
                  isolate,
                  static_cast<uint64_t>(reinterpret_cast<uintptr_t>(fn->ptr))))
           .FromMaybe(false)) {
    return MaybeLocal<Function>();
  }

  // Internal properties are keyed by per-isolate Symbols (see
  // `env_properties.h`) to keep them out of string-key reflection, and the
  // `ReadOnly | DontEnum | DontDelete` attribute set blocks user code from
  // reading, modifying, or deleting them.
  PropertyAttribute internal_attrs =
      static_cast<PropertyAttribute>(ReadOnly | DontEnum | DontDelete);

  if (use_sb) {
    // SharedBuffer layout is intentionally fixed-width: slot 0 stores the
    // return value and slots 1..N store argument payloads. The JS wrapper and
    // InvokeFunctionSB share this exact layout.
    size_t sb_size = 8 * (fn->args.size() + 1);
    Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, sb_size);
    // The shared_ptr to the backing store keeps the memory alive while
    // FFIFunctionInfo still references it.
    info->sb_backing = ab->GetBackingStore();

    if (!ret->DefineOwnProperty(
                context, env->ffi_sb_shared_buffer_symbol(), ab, internal_attrs)
             .FromMaybe(false)) {
      return MaybeLocal<Function>();
    }

    // Signatures with pointer args also expose a slow-path invoker bound
    // to the same FFIFunctionInfo. The JS wrapper routes through it when a
    // pointer argument is anything other than a BigInt, null, or undefined
    // (strings, Buffers, ArrayBuffers, and ArrayBufferViews).
    if (has_ptr_args) {
      Local<Function> slow_fn;
      if (!Function::New(context,
                         DynamicLibrary::InvokeFunction,
                         info->object(),
                         0,
                         v8::ConstructorBehavior::kThrow)
               .ToLocal(&slow_fn)) {
        return MaybeLocal<Function>();
      }
      if (!ret->DefineOwnProperty(context,
                                  env->ffi_sb_invoke_slow_symbol(),
                                  slow_fn,
                                  internal_attrs)
               .FromMaybe(false)) {
        return MaybeLocal<Function>();
      }
    }

    // Attach the original signature type names so the JS wrapper can
    // rebuild the signature from a raw function when the caller did not
    // pass arguments and return explicitly. The `lib.functions` accessor
    // path relies on this.
    Local<Value> args_arr;
    if (!ToV8Value(context, fn->arg_type_names, isolate).ToLocal(&args_arr)) {
      return MaybeLocal<Function>();
    }
    if (!ret->DefineOwnProperty(context,
                                env->ffi_sb_arguments_symbol(),
                                args_arr,
                                internal_attrs)
             .FromMaybe(false)) {
      return MaybeLocal<Function>();
    }

    Local<Value> return_name;
    if (!ToV8Value(context, fn->return_type_name, isolate)
             .ToLocal(&return_name)) {
      return MaybeLocal<Function>();
    }
    if (!ret->DefineOwnProperty(context,
                                env->ffi_sb_return_symbol(),
                                return_name,
                                internal_attrs)
             .FromMaybe(false)) {
      return MaybeLocal<Function>();
    }
  }

  if (needs_fast_argument_wrapper || needs_fast_buffer_invoke) {
    // Fast API wrappers need only the parameter type names. Result conversion
    // is still handled by V8's CFunction metadata, unlike the SharedBuffer path
    // which must also know how to read slot 0.
    Local<Value> arguments_arr;
    if (!ToV8Value(context, fn->arg_type_names, isolate)
             .ToLocal(&arguments_arr)) {
      return MaybeLocal<Function>();
    }
    if (!ret->DefineOwnProperty(context,
                                env->ffi_fast_arguments_symbol(),
                                arguments_arr,
                                internal_attrs)
             .FromMaybe(false)) {
      return MaybeLocal<Function>();
    }
  }

  if (needs_fast_buffer_invoke) {
    // Build an alternate CFunction that describes the pointer-like argument as
    // a V8 buffer value. The JS wrapper dispatches here only when the runtime
    // argument is Buffer/ArrayBuffer-backed memory.
    std::shared_ptr<FFIFunction> fast_buffer_fn =
        CloneWithFastBufferArgNames(fn);
    if (!CreateFastFFIMetadata(*fast_buffer_fn, &fn->closed, isolate)
             .MoveTo(&info->fast_buffer_metadata)) {
      return {};
    }
    if (info->fast_buffer_metadata != nullptr) {
      // Store the secondary invoker on the primary raw function under a hidden
      // Symbol. Keeping it separate avoids overloading SharedBuffer slow-path
      // metadata for Fast API routing.
      Local<FunctionTemplate> tmpl =
          FunctionTemplate::New(isolate,
                                DynamicLibrary::InvokeFunction,
                                info->object(),
                                Local<v8::Signature>(),
                                fn->args.size(),
                                v8::ConstructorBehavior::kThrow,
                                v8::SideEffectType::kHasSideEffect,
                                &info->fast_buffer_metadata->c_function);
      Local<Function> fast_buffer_invoke;
      if (!GetUncachedFunction(env, tmpl).ToLocal(&fast_buffer_invoke)) {
        return MaybeLocal<Function>();
      }
      if (!ret->DefineOwnProperty(context,
                                  env->ffi_fast_buffer_invoke_symbol(),
                                  fast_buffer_invoke,
                                  internal_attrs)
               .FromMaybe(false)) {
        return MaybeLocal<Function>();
      }
    }
  }

  return ret;
}

void DynamicLibrary::New(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  if (!args.IsConstructCall()) {
    return THROW_ERR_CONSTRUCT_CALL_REQUIRED(
        env,
        "Class constructor DynamicLibrary cannot be invoked without 'new'");
  }

  THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");

#ifndef _WIN32
  if (args.Length() < 1 || (!args[0]->IsString() && !args[0]->IsNull())) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string or null");
    return;
  }
#else
  if (args.Length() < 1 || !args[0]->IsString()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Library path must be a string");
    return;
  }
#endif

  const char* library_path = nullptr;
  DynamicLibrary* lib = new DynamicLibrary(env, args.This());

  if (args[0]->IsString()) {
    Utf8Value filename(env->isolate(), args[0]);
    if (ThrowIfContainsNullBytes(env, filename, "Library path")) {
      return;
    }
    lib->path_ = filename.ToString();
    library_path = lib->path_.c_str();
  }

  // On the internal path args[1] carries the library's bytes, for a library
  // that lives somewhere the dynamic loader cannot open by path (a virtual
  // file system). Materialize them into a private, self-cleaning image - the
  // same mechanism process.dlopen() uses for such native addons - and load
  // that, while still reporting the library's own path in `library.path` and
  // any error.
  binding::AddonImage image;
  if (args.Length() > 1 && !args[1]->IsUndefined()) {
    if (!args[1]->IsArrayBufferView()) {
      THROW_ERR_INVALID_ARG_TYPE(
          env, "Library binary must be a Buffer, TypedArray, or DataView");
      return;
    }
    // Loading from bytes materializes them into an image in the temporary
    // directory, so this needs write access there on top of the FFI
    // permission checked above. The check does not depend on whether the
    // image actually reaches the file system on this platform (Linux uses an
    // anonymous memfd): what a program must be granted should not vary by
    // platform.
    THROW_IF_INSUFFICIENT_PERMISSIONS(
        env,
        permission::PermissionScope::kFileSystemWrite,
        binding::AddonImage::TempDir());
    ArrayBufferViewContents<char> binary(args[1]);
    if (!image.Materialize(binary.data(), binary.length())) {
      THROW_ERR_FFI_CALL_FAILED(
          env, "dlopen failed: %s: %s", image.errmsg().c_str(), library_path);
      return;
    }
    library_path = image.path().c_str();
  }

  CHECK(lib->is_closed());
  // Open the library
  const bool opened = uv_dlopen(library_path, &lib->lib_) == 0;
  image.AfterOpen(opened,
                  opened ? static_cast<void*>(lib->lib_.handle) : nullptr);
  if (!opened) {
    THROW_ERR_FFI_CALL_FAILED(env, "dlopen failed: %s", uv_dlerror(&lib->lib_));
    // Free the error message. Close() skips uv_dlclose() for a null handle.
    uv_dlclose(&lib->lib_);
    return;
  }
}

void DynamicLibrary::Close(const FunctionCallbackInfo<Value>& args) {
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
  // Closing a library from one of its active callbacks is unsupported and
  // dangerous. Callbacks must return before the owning library is closed.
  lib->Close();
}

void DynamicLibrary::InvokeFunction(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  FFIFunctionInfo* info = Unwrap<FFIFunctionInfo>(args.Data());
  CHECK_NOT_NULL(info);
  FFIFunction* fn = info->fn.get();

  if (fn == nullptr || fn->closed || fn->ptr == nullptr) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  // Convert arguments
  unsigned int expected_args = fn->args.size();
  unsigned int provided_args = args.Length();

  if (provided_args != expected_args) {
    THROW_ERR_INVALID_ARG_VALUE(env,
                                "Invalid argument count: expected %s, got %s",
                                expected_args,
                                provided_args);
    return;
  }

  std::vector<uint64_t> values(expected_args, 0);
  std::vector<void*> ffi_args(expected_args, nullptr);
  std::vector<std::string> strings;

  for (unsigned int i = 0; i < expected_args; i++) {
    FFIArgumentCategory res;

    if (!ToFFIArgument(env, i, fn->args[i], args[i], &values[i]).To(&res)) {
      return;
    }

    // The argument is a string, we need to copy
    if (res == FFIArgumentCategory::String) {
      Utf8Value str(env->isolate(), args[i]);

      if (*str == nullptr) {
        THROW_ERR_INVALID_ARG_TYPE(env, "Argument %s must be a string", i);
        return;
      }

      if (ThrowIfContainsNullBytes(env, str, "Argument " + std::to_string(i))) {
        return;
      }

      if (strings.empty()) {
        // Keep string pointers stable as subsequent arguments are converted.
        strings.reserve(expected_args);
      }
      strings.push_back(*str);
      values[i] = reinterpret_cast<uint64_t>(strings.back().c_str());
      ffi_args[i] = &values[i];
    } else {
      ffi_args[i] = &values[i];
    }
  }

  void* result = nullptr;

  if (fn->return_type->type != FFI_TYPE_VOID) {
    result = Malloc(GetFFIReturnValueStorageSize(fn->return_type));
  }

  fn->Invoke(result, ffi_args.data());

  // Return result back to Javascript
  ToJSReturnValue(env, args, fn->return_type, result);
  free(result);
}

void DynamicLibrary::InvokeFunctionSB(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  FFIFunctionInfo* info = Unwrap<FFIFunctionInfo>(args.Data());
  CHECK_NOT_NULL(info);
  FFIFunction* fn = info->fn.get();

  if (fn == nullptr || fn->closed || fn->ptr == nullptr) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  // Arguments reach the native invoker through the shared buffer, not
  // through V8. The JS wrapper always calls the raw function as `rawFn()`
  // so any non-zero argument count indicates that user code reached the
  // raw SB function directly and is about to read stale buffer contents.
  if (args.Length() != 0) {
    THROW_ERR_INVALID_ARG_VALUE(
        env,
        "SB-invoked FFI functions receive arguments through the shared "
        "buffer, not as JavaScript arguments");
    return;
  }

  // A failure of either CHECK means the SB invoker ran against a function
  // that `CreateFunction` did not set up for the fast path, which is a
  // contract violation. They stay enabled in Release because each FFI call
  // is already dominated by `ffi_call` itself.
  CHECK(info->sb_backing);
  CHECK_EQ(info->sb_backing->ByteLength(), 8u * (info->fn->args.size() + 1));

  uint8_t* buffer = static_cast<uint8_t*>(info->sb_backing->Data());
  unsigned int nargs = fn->args.size();

  // Layout is 8 bytes per slot. The return value lives at offset 0 and
  // argument i lives at offset 8*(i+1).
  std::vector<uint64_t> values(nargs, 0);
  std::vector<void*> ffi_args(nargs, nullptr);

  for (unsigned int i = 0; i < nargs; i++) {
    ReadFFIArgFromBuffer(fn->args[i], buffer, 8 * (i + 1), &values[i]);
    ffi_args[i] = &values[i];
  }

  // The storage must cover both the ffi_arg width that libffi uses for
  // promoted small integer returns and the 8 bytes needed for non-promoted
  // SB-eligible returns like f64, i64, and u64. `sizeof(ffi_arg)` is only
  // 4 on 32-bit ARM, so take the max.
  constexpr size_t kSBResultStorageSize =
      sizeof(ffi_arg) > 8 ? sizeof(ffi_arg) : 8;
  alignas(8) uint8_t result_storage[kSBResultStorageSize] = {0};
  void* result = (fn->return_type != &ffi_type_void) ? result_storage : nullptr;

  fn->Invoke(result, ffi_args.data());

  if (result != nullptr) {
    WriteFFIReturnToBuffer(fn->return_type, result, buffer, 0);
  }
}

// This is the function that will be called by libffi when a callback
// is invoked from a dlopen library. It converts the arguments to JavaScript
// values and calls the original JavaScript callback function.
// It also handles the return value and exceptions properly.
// Note that since this function is called from native code, it must not throw
// exceptions or return promises, as there is no defined way to propagate them
// back to the caller.
// If such cases occur, the process will be aborted to avoid undefined behavior.
void DynamicLibrary::InvokeCallback(ffi_cif* cif,
                                    void* ret,
                                    void** args,
                                    void* user_data) {
  FFICallback* cb = static_cast<FFICallback*>(user_data);

  // It is unsupported and dangerous for a callback to unregister itself or
  // close its owning library while executing. The current invocation must
  // return before teardown APIs are used.
  if (cb->owner->is_closed() || cb->ptr == nullptr) {
    if (ret != nullptr && cb->return_type->size > 0) {
      std::memset(ret, 0, GetFFIReturnValueStorageSize(cb->return_type));
    }
    return;
  }

  if (std::this_thread::get_id() != cb->thread_id) {
    FPrintF(stderr,
            "Callbacks can only be invoked on the system thread they were "
            "created on\n");
    ABORT();
  }

  Environment* env = cb->env;
  Isolate* isolate = env->isolate();

  HandleScope handle_scope(isolate);
  Local<Context> context = env->context();

  if (cb->fn.IsEmpty()) {
    if (ret != nullptr && cb->return_type->size > 0) {
      std::memset(ret, 0, GetFFIReturnValueStorageSize(cb->return_type));
    }
    return;
  }

  size_t expected_args = cb->args.size();
  LocalVector<Value> callback_args(isolate, expected_args);

  // libffi always points `args[i]` at its own storage for the value of
  // argument `i`, so the slot pointers themselves are never null. A NULL
  // pointer argument surfaces as the BigInt `0n` via ToJSArgument.
  for (size_t i = 0; i < expected_args; i++) {
    callback_args[i] = ToJSArgument(isolate, cb->args[i], args[i]);
  }

  // When native code invokes the callback from the event loop (for example
  // through a libuv handle), no callback scope is active. Open one so that
  // nextTick and microtasks queued by the callback are drained when it
  // returns, as for Node.js's own callbacks. Callbacks invoked synchronously
  // during a call into native code already run inside the caller's scope.
  Context::Scope context_scope(context);
  std::optional<InternalCallbackScope> callback_scope;
  if (env->async_callback_scope_depth() == 0) {
    callback_scope.emplace(env, Object::New(isolate), async_context{0, 0});
  }

  TryCatch try_catch(isolate);
  Local<Function> callback = Local<Function>::New(isolate, cb->fn);
  MaybeLocal<Value> result = callback->Call(
      context, Undefined(isolate), expected_args, callback_args.data());

  // Termination (worker.terminate(), process.exit() in a Worker, or
  // environment teardown) is not an exception thrown by the callback.
  // Return a zeroed result and let the caller unwind.
  if (try_catch.HasTerminated()) {
    if (ret != nullptr && cb->return_type->size > 0) {
      std::memset(ret, 0, GetFFIReturnValueStorageSize(cb->return_type));
    }
    return;
  }

  // Handle exceptions by crashing (can't propagate across FFI boundary)
  if (try_catch.HasCaught()) {
    FPrintF(stderr, "Callbacks cannot throw an exception\n");
    ABORT();
  }

  Local<Value> result_val;
  if (!result.ToLocal(&result_val)) {
    if (try_catch.HasCaught()) {
      FPrintF(stderr, "Callbacks cannot return an exception\n");
      ABORT();
    }
    return;
  }

  if (result_val->IsPromise()) {
    FPrintF(stderr, "Callbacks cannot return promises\n");
    ABORT();
  }

  if (!ToFFIReturnValue(result_val, cb->return_type, ret)) {
    FPrintF(stderr, "Callback returned invalid value for declared FFI type\n");
    ABORT();
  }
}

void DynamicLibrary::GetPath(const FunctionCallbackInfo<Value>& args) {
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  Local<Value> path;
  if (!ToV8Value(lib->env()->context(), lib->path_, args.GetIsolate())
           .ToLocal(&path)) {
    return;
  }

  args.GetReturnValue().Set(path);
}

void DynamicLibrary::GetFunction(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();

  if (args.Length() < 1 || !args[0]->IsString()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Function name must be a string");
    return;
  }

  if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
    return;
  }

  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
  Utf8Value name(isolate, args[0]);
  if (ThrowIfContainsNullBytes(env, name, "Function name")) {
    return;
  }
  PreparedFunction prepared;

  Local<Object> signature = args[1].As<Object>();
  if (!lib->PrepareFunction(env, *name, signature).To(&prepared)) {
    return;
  }
  auto [fn, should_cache_symbol, should_cache_function] = prepared;

  if (should_cache_symbol) {
    lib->symbols_.emplace(*name, fn->ptr);
  }
  if (should_cache_function) {
    lib->functions_.emplace(*name, fn);
  }

  MaybeLocal<Function> maybe_ret = lib->CreateFunction(env, *name, fn);
  Local<Function> ret;
  if (!maybe_ret.ToLocal(&ret)) {
    return;
  }
  args.GetReturnValue().Set(ret);
}

void DynamicLibrary::ToFunction(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");

  if (args.Length() < 1 || !args[0]->IsBigInt()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Function pointer must be a bigint");
    return;
  }
  bool lossless;
  uint64_t address = args[0].As<BigInt>()->Uint64Value(&lossless);
  if (!lossless || address == 0 ||
      address > static_cast<uint64_t>(std::numeric_limits<uintptr_t>::max())) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Invalid function pointer");
    return;
  }
  if (args.Length() < 2 || !args[1]->IsObject() || args[1]->IsArray()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Function signature must be an object");
    return;
  }

  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }
  Local<Object> signature = args[1].As<Object>();
  PreparedFunction prepared;
  void* ptr = reinterpret_cast<void*>(static_cast<uintptr_t>(address));
  if (!lib->PrepareFunction(env, "<pointer>", signature, ptr).To(&prepared)) {
    return;
  }
  THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");
  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  auto fn = std::move(prepared.fn);
  if (lib->pointer_functions_ == nullptr) {
    lib->pointer_functions_ =
        std::make_shared<std::unordered_set<FFIFunction*>>();
  }
  fn->pointer_registry = lib->pointer_functions_;
  lib->pointer_functions_->insert(fn.get());
  Local<Function> ret;
  if (!lib->BuildFunction(env, "<pointer>", fn, false).ToLocal(&ret)) {
    lib->pointer_functions_->erase(fn.get());
    fn->ptr = nullptr;
    return;
  }
  args.GetReturnValue().Set(ret);
}

void DynamicLibrary::GetFunctions(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();
  Local<Context> context = env->context();
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  Local<Object> functions = Object::New(isolate);
  if (!functions->SetPrototype(context, Null(isolate)).FromMaybe(false)) {
    return;
  }

  if (args.Length() > 0) {
    if (!args[0]->IsObject() || args[0]->IsArray()) {
      THROW_ERR_INVALID_ARG_TYPE(env, "Functions signatures must be an object");
      return;
    }

    Local<Object> signatures = args[0].As<Object>();
    Local<Array> keys;
    if (!signatures->GetOwnPropertyNames(context).ToLocal(&keys)) {
      return;
    }

    std::vector<ResolvedFunction> pending;
    pending.reserve(keys->Length());

    for (uint32_t i = 0; i < keys->Length(); i++) {
      Local<Value> key;
      Local<Value> signature;

      if (!keys->Get(context, i).ToLocal(&key)) {
        return;
      }

      Utf8Value name(isolate, key);
      if (ThrowIfContainsNullBytes(env, name, "Function name")) {
        return;
      }

      if (!signatures->Get(env->context(), key).ToLocal(&signature)) {
        return;
      }

      if (!signature->IsObject() || signature->IsArray()) {
        THROW_ERR_INVALID_ARG_TYPE(
            env, "Signature of function %s must be an object", name);
        return;
      }

      PreparedFunction prepared;

      Local<Object> signature_object = signature.As<Object>();
      if (!lib->PrepareFunction(env, *name, signature_object).To(&prepared)) {
        return;
      }
      auto [fn, should_cache_symbol, should_cache_function] = prepared;

      pending.push_back(ResolvedFunction{
          .name = *name,
          .fn = fn,
          .should_cache_symbol = should_cache_symbol,
          .should_cache_function = should_cache_function,
      });
    }

    for (const auto& item : pending) {
      if (item.should_cache_symbol) {
        lib->symbols_.emplace(item.name, item.fn->ptr);
      }
      if (item.should_cache_function) {
        lib->functions_.emplace(item.name, item.fn);
      }
    }

    for (const auto& item : pending) {
      MaybeLocal<Function> maybe_ret =
          lib->CreateFunction(env, item.name, item.fn);
      Local<Function> ret;
      if (!maybe_ret.ToLocal(&ret)) {
        return;
      }

      Local<Value> name_string;
      if (!ToV8Value(env->context(), item.name, env->isolate())
               .ToLocal(&name_string)) {
        return;
      }

      if (!functions->Set(context, name_string.As<String>(), ret)
               .FromMaybe(false)) {
        return;
      }
    }
  } else {
    for (const auto& entry : lib->functions_) {
      MaybeLocal<Function> maybe_fn =
          lib->CreateFunction(env, entry.first, entry.second);
      Local<Function> fn;
      if (!maybe_fn.ToLocal(&fn)) {
        return;
      }

      Local<Value> name_string;
      if (!ToV8Value(env->context(), entry.first, env->isolate())
               .ToLocal(&name_string)) {
        return;
      }

      if (!functions->Set(context, name_string.As<String>(), fn)
               .FromMaybe(false)) {
        return;
      }
    }
  }

  args.GetReturnValue().Set(functions);
}

void DynamicLibrary::GetSymbol(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();

  if (args.Length() < 1 || !args[0]->IsString()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "Symbol name must be a string");
    return;
  }

  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
  Utf8Value name(isolate, args[0]);
  if (ThrowIfContainsNullBytes(env, name, "Symbol name")) {
    return;
  }
  void* ptr;

  if (!lib->ResolveSymbol(env, *name).To(&ptr)) {
    return;
  }

  lib->symbols_.emplace(*name, ptr);

  args.GetReturnValue().Set(BigInt::NewFromUnsigned(
      isolate, static_cast<uint64_t>(reinterpret_cast<uintptr_t>(ptr))));
}

void DynamicLibrary::GetSymbols(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();
  Local<Context> context = env->context();
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  Local<Object> symbols = Object::New(isolate);
  if (!symbols->SetPrototype(context, Null(isolate)).FromMaybe(false)) {
    return;
  }
  for (const auto& entry : lib->symbols_) {
    Local<Value> symbol_key;
    if (!ToV8Value(env->context(), entry.first, env->isolate())
             .ToLocal(&symbol_key)) {
      return;
    }

    if (!symbols
             ->Set(context,
                   symbol_key.As<String>(),
                   BigInt::NewFromUnsigned(
                       isolate,
                       static_cast<uint64_t>(
                           reinterpret_cast<uintptr_t>(entry.second))))
             .FromMaybe(false)) {
      return;
    }
  }

  args.GetReturnValue().Set(symbols);
}

void DynamicLibrary::RegisterCallback(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();

  ffi_type* return_type = &ffi_type_void;
  std::vector<ffi_type*> callback_args;
  Local<Function> fn;

  if (args.Length() < 1) {
    THROW_ERR_INVALID_ARG_TYPE(
        env, "First argument must be a function or a signature object");
    return;
  }

  if (args[0]->IsFunction()) {
    if (args.Length() > 1) {
      THROW_ERR_INVALID_ARG_TYPE(env, "Callback must be the last argument");
      return;
    }
    fn = args[0].As<Function>();
  } else {
    if (!args[0]->IsObject() || args[0]->IsArray()) {
      THROW_ERR_INVALID_ARG_TYPE(
          env, "First argument must be a function or a signature object");
      return;
    }

    if (args.Length() < 2 || !args[1]->IsFunction()) {
      THROW_ERR_INVALID_ARG_TYPE(env, "Second argument must be a function");
      return;
    }

    FunctionSignature parsed;
    if (!ParseFunctionSignature(env, "<callback>", args[0].As<Object>())
             .To(&parsed)) {
      return;
    }

    return_type = parsed.return_type;
    callback_args = std::move(parsed.args);

    fn = args[1].As<Function>();
  }

  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());
  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  auto callback = std::unique_ptr<FFICallback>(
      new FFICallback{.owner = BaseObjectPtr<DynamicLibrary>(lib),
                      .env = env,
                      .thread_id = std::this_thread::get_id(),
                      .fn = Global<Function>(isolate, fn),
                      .closure = nullptr,
                      .ptr = nullptr,
                      .cif = {},
                      .args = std::move(callback_args),
                      .return_type = return_type});

  callback->closure = static_cast<ffi_closure*>(
      ffi_closure_alloc(sizeof(ffi_closure), &callback->ptr));

  if (callback->closure == nullptr) {
    // libffi may use platform-specific static trampolines, so only diagnose
    // missing RX memory after its own closure allocation has failed.
    if (!IsJitMemorySupported()) {
      THROW_ERR_RX_MEMORY_NOT_SUPPORTED(
          env,
          "Executable memory is not supported in this environment, "
          "but is required for FFI callbacks");
      return;
    }
    THROW_ERR_FFI_CALL_FAILED(env, "ffi_closure_alloc failed");
    return;
  }

  ffi_status status;
  status = ffi_prep_cif(&callback->cif,
                        FFI_DEFAULT_ABI,
                        callback->args.size(),
                        callback->return_type,
                        callback->args.data());
  if (status != FFI_OK) {
    const char* msg = "ffi_prep_cif failed";
    switch (status) {
      case FFI_BAD_TYPEDEF:
        msg = "ffi_prep_cif failed: bad typedef";
        break;
      case FFI_BAD_ABI:
        msg = "ffi_prep_cif failed: bad ABI";
        break;
      default:
        msg = "ffi_prep_cif failed: unknown error";
        break;
    }

    THROW_ERR_FFI_CALL_FAILED(env, msg);
    return;
  }

  status = ffi_prep_closure_loc(callback->closure,
                                &callback->cif,
                                DynamicLibrary::InvokeCallback,
                                callback.get(),
                                callback->ptr);
  if (status != FFI_OK) {
    const char* msg = "ffi_prep_closure_loc failed";
    switch (status) {
      case FFI_BAD_TYPEDEF:
        msg = "ffi_prep_closure_loc failed: bad typedef";
        break;
      case FFI_BAD_ABI:
        msg = "ffi_prep_closure_loc failed: bad ABI";
        break;
      default:
        msg = "ffi_prep_closure_loc failed: unknown error";
        break;
    }

    THROW_ERR_FFI_CALL_FAILED(env, msg);
    return;
  }

  auto ret = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(callback->ptr));
  lib->callbacks_.emplace(callback->ptr, std::move(callback));
  args.GetReturnValue().Set(BigInt::NewFromUnsigned(isolate, ret));
}

void DynamicLibrary::UnregisterCallback(
    const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  if (args.Length() < 1 || !args[0]->IsBigInt()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
    return;
  }

  uintptr_t raw_ptr;
  if (!GetValidatedPointerAddress(env, args[0], "first argument")
           .To(&raw_ptr)) {
    return;
  }

  void* ptr = reinterpret_cast<void*>(raw_ptr);
  auto existing = lib->callbacks_.find(ptr);

  if (existing == lib->callbacks_.end()) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
    return;
  }

  // This releases the callback trampoline immediately. If foreign code still
  // retains and invokes the pointer afterwards, the behavior is undefined, not
  // allowed, and dangerous: it can crash the process, produce incorrect
  // output, or corrupt memory. Unregistering a callback while it is currently
  // executing is also unsupported and dangerous.
  lib->callbacks_.erase(existing);
}

void DynamicLibrary::RefCallback(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  if (args.Length() < 1 || !args[0]->IsBigInt()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
    return;
  }

  uintptr_t raw_ptr;
  if (!GetValidatedPointerAddress(env, args[0], "first argument")
           .To(&raw_ptr)) {
    return;
  }

  void* ptr = reinterpret_cast<void*>(raw_ptr);
  auto existing = lib->callbacks_.find(ptr);

  if (existing == lib->callbacks_.end()) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
    return;
  }

  // The callback function may already have been collected after a previous
  // unrefCallback() call. The persistent handle is empty in that case, and
  // ClearWeak() on an empty handle dereferences a null slot. There is also no
  // function left to make strong again.
  if (existing->second->fn.IsEmpty()) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
    return;
  }

  existing->second->fn.ClearWeak();
}

void DynamicLibrary::UnrefCallback(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  DynamicLibrary* lib = Unwrap<DynamicLibrary>(args.This());

  if (lib->is_closed()) {
    THROW_ERR_FFI_LIBRARY_CLOSED(env);
    return;
  }

  if (args.Length() < 1 || !args[0]->IsBigInt()) {
    THROW_ERR_INVALID_ARG_TYPE(env, "The first argument must be a bigint");
    return;
  }

  uintptr_t raw_ptr;
  if (!GetValidatedPointerAddress(env, args[0], "first argument")
           .To(&raw_ptr)) {
    return;
  }

  void* ptr = reinterpret_cast<void*>(raw_ptr);
  auto existing = lib->callbacks_.find(ptr);

  if (existing == lib->callbacks_.end()) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
    return;
  }

  // The callback function may already have been collected by a previous
  // unrefCallback() call. The persistent handle is empty in that case, and
  // SetWeak() on an empty handle dereferences a null slot.
  if (existing->second->fn.IsEmpty()) {
    THROW_ERR_INVALID_ARG_VALUE(env, "Callback not found");
    return;
  }

  existing->second->fn.SetWeak();
}

Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
    Environment* env) {
  Local<FunctionTemplate> tmpl =
      env->ffi_dynamic_library_constructor_template();

  if (tmpl.IsEmpty()) {
    Isolate* isolate = env->isolate();
    enum PropertyAttribute attributes =
        static_cast<PropertyAttribute>(ReadOnly | DontDelete);

    tmpl = NewFunctionTemplate(isolate, DynamicLibrary::New);
    tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "DynamicLibrary"));
    tmpl->InstanceTemplate()->SetInternalFieldCount(
        DynamicLibrary::kInternalFieldCount);
    Local<Signature> signature = Signature::New(isolate, tmpl);

    tmpl->InstanceTemplate()->SetAccessorProperty(
        env->path_string(),
        FunctionTemplate::New(
            isolate, DynamicLibrary::GetPath, Local<Value>(), signature),
        Local<FunctionTemplate>(),
        attributes);

    tmpl->InstanceTemplate()->SetAccessorProperty(
        FIXED_ONE_BYTE_STRING(isolate, "symbols"),
        FunctionTemplate::New(
            isolate, DynamicLibrary::GetSymbols, Local<Value>(), signature),
        Local<FunctionTemplate>(),
        attributes);

    // `functions` lives on the prototype template rather than the instance
    // template so `lib/ffi.js` can replace it via `Object.defineProperty`
    // on the prototype. The attribute set omits `DontDelete` for the same
    // reason.
    tmpl->PrototypeTemplate()->SetAccessorProperty(
        FIXED_ONE_BYTE_STRING(isolate, "functions"),
        FunctionTemplate::New(
            isolate, DynamicLibrary::GetFunctions, Local<Value>(), signature),
        Local<FunctionTemplate>(),
        static_cast<PropertyAttribute>(ReadOnly));

    SetProtoMethod(isolate, tmpl, "close", DynamicLibrary::Close);
    SetProtoDispose(isolate, tmpl, DynamicLibrary::Close);
    SetProtoMethod(isolate, tmpl, "getFunction", DynamicLibrary::GetFunction);
    SetProtoMethod(isolate, tmpl, "toFunction", DynamicLibrary::ToFunction);
    SetProtoMethod(isolate, tmpl, "getFunctions", DynamicLibrary::GetFunctions);
    SetProtoMethod(isolate, tmpl, "getSymbol", DynamicLibrary::GetSymbol);
    SetProtoMethod(isolate, tmpl, "getSymbols", DynamicLibrary::GetSymbols);
    SetProtoMethod(
        isolate, tmpl, "registerCallback", DynamicLibrary::RegisterCallback);
    SetProtoMethod(isolate,
                   tmpl,
                   "unregisterCallback",
                   DynamicLibrary::UnregisterCallback);
    SetProtoMethod(isolate, tmpl, "refCallback", DynamicLibrary::RefCallback);
    SetProtoMethod(
        isolate, tmpl, "unrefCallback", DynamicLibrary::UnrefCallback);

    env->set_ffi_dynamic_library_constructor_template(tmpl);
  }

  return tmpl;
}

void GetCurrentEventLoop(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Isolate* isolate = env->isolate();

  THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");

  args.GetReturnValue().Set(BigInt::NewFromUnsigned(
      isolate,
      static_cast<uint64_t>(reinterpret_cast<uintptr_t>(env->event_loop()))));
}

// Module initialization.
static void Initialize(Local<Object> target,
                       Local<Value> unused,
                       Local<Context> context,
                       void* priv) {
  Environment* env = Environment::GetCurrent(context);

  // Create the DynamicLibrary template
  Local<FunctionTemplate> dl_tmpl = DynamicLibrary::GetConstructorTemplate(env);
  SetConstructorFunction(context,
                         target,
                         "DynamicLibrary",
                         dl_tmpl,
                         SetConstructorFunctionFlag::NONE);
  SetMethod(context, target, "toString", ToString);
  SetMethod(context, target, "toBuffer", ToBuffer);
  SetMethod(context, target, "toArrayBuffer", ToArrayBuffer);
  SetMethod(context, target, "exportBytes", ExportBytes);
  SetMethod(context, target, "getRawPointer", GetRawPointer);
  SetMethod(context, target, "getCurrentEventLoop", GetCurrentEventLoop);

  SetMethod(context, target, "getInt8", GetInt8);
  SetMethod(context, target, "getUint8", GetUint8);
  SetMethod(context, target, "getInt16", GetInt16);
  SetMethod(context, target, "getUint16", GetUint16);
  SetMethod(context, target, "getInt32", GetInt32);
  SetMethod(context, target, "getUint32", GetUint32);
  SetMethod(context, target, "getInt64", GetInt64);
  SetMethod(context, target, "getUint64", GetUint64);
  SetMethod(context, target, "getFloat32", GetFloat32);
  SetMethod(context, target, "getFloat64", GetFloat64);

  SetMethod(context, target, "setInt8", SetInt8);
  SetMethod(context, target, "setUint8", SetUint8);
  SetMethod(context, target, "setInt16", SetInt16);
  SetMethod(context, target, "setUint16", SetUint16);
  SetMethod(context, target, "setInt32", SetInt32);
  SetMethod(context, target, "setUint32", SetUint32);
  SetMethod(context, target, "setInt64", SetInt64);
  SetMethod(context, target, "setUint64", SetUint64);
  SetMethod(context, target, "setFloat32", SetFloat32);
  SetMethod(context, target, "setFloat64", SetFloat64);

  // ToFFIType maps `char` to sint8 or uint8 based on `CHAR_MIN < 0` at C++
  // build time. Exposing the same decision to JS lets the shared-buffer
  // wrapper's range check match `ToFFIArgument` on every platform.
  Isolate* isolate = env->isolate();
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "charIsSigned"),
            Boolean::New(isolate, CHAR_MIN < 0))
      .Check();

  // The JavaScript fast paths use `uintptrMax` to reject pointer BigInts that
  // would otherwise be silently truncated by V8 or, on 32-bit platforms, by
  // `ReadFFIArgFromBuffer`'s `memcpy(..., type->size, ...)`. The slow path
  // rejects the same values through `ToFFIArgument`.
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "uintptrMax"),
            v8::BigInt::NewFromUnsigned(
                isolate,
                static_cast<uint64_t>(std::numeric_limits<uintptr_t>::max())))
      .Check();

  // Per-isolate Symbols used by `lib/internal/ffi-shared-buffer.js` to key
  // shared-buffer internal state on raw FFI functions.
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kSbSharedBuffer"),
            env->ffi_sb_shared_buffer_symbol())
      .Check();
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kSbInvokeSlow"),
            env->ffi_sb_invoke_slow_symbol())
      .Check();
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kSbArguments"),
            env->ffi_sb_arguments_symbol())
      .Check();
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kSbReturn"),
            env->ffi_sb_return_symbol())
      .Check();
  // Fast API wrappers use separate metadata Symbols so pointer-conversion
  // routing does not depend on SharedBuffer internals. These are created here
  // (at runtime, on first `internalBinding('ffi')`) instead of being declared
  // in env_properties.h, so they are not allocated while the startup snapshot
  // is being built. Allocating Symbols during snapshot serialization advances
  // the isolate's identity-hash RNG and shifts the identity hashes baked into
  // the snapshot for Object.prototype / Function.prototype, which can make a
  // function map and a plain-object map collide in V8's NormalizedMapCache.
  if (env->ffi_fast_arguments_symbol().IsEmpty()) {
    env->set_ffi_fast_arguments_symbol(v8::Symbol::New(
        isolate, FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_arguments_symbol")));
  }
  if (env->ffi_fast_buffer_invoke_symbol().IsEmpty()) {
    env->set_ffi_fast_buffer_invoke_symbol(v8::Symbol::New(
        isolate,
        FIXED_ONE_BYTE_STRING(isolate, "ffi_fast_buffer_invoke_symbol")));
  }
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kFastArguments"),
            env->ffi_fast_arguments_symbol())
      .Check();
  target
      ->Set(context,
            FIXED_ONE_BYTE_STRING(isolate, "kFastBufferInvoke"),
            env->ffi_fast_buffer_invoke_symbol())
      .Check();
}

}  // namespace ffi
}  // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(ffi, node::ffi::Initialize)

#endif  // HAVE_FFI