Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions NativeScript/NapiRuntime.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// NapiRuntime.h
// NativeScript
//
// Copyright © 2026 Progress. All rights reserved.
//

#pragma once
#import <Foundation/Foundation.h>

#include "napi/vendor/node_api.h"

#ifdef __cplusplus
extern "C" {
#endif

// The Node-API environment of the runtime on the calling thread, or NULL when
// this thread has no runtime (or its runtime has torn down). Each runtime —
// the main one and every Worker — owns a separate env.
napi_env NativeScriptNapiEnv(void);

#ifdef __cplusplus
}
#endif

@interface NapiRuntime : NSObject
+ (napi_env)env;
@end
25 changes: 25 additions & 0 deletions NativeScript/NapiRuntime.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// NapiRuntime.mm
// NativeScript
//
// Copyright © 2026 Progress. All rights reserved.
//

#import "NapiRuntime.h"

#include "runtime/Runtime.h"

extern "C" napi_env NativeScriptNapiEnv(void) {
// The thread-local can go stale when a Runtime is destroyed on a different
// thread than the one that created it, so the env is resolved through the
// registry without dereferencing the pointer outside its lock.
return tns::Runtime::GetNapiEnvIfAlive(tns::Runtime::GetCurrentRuntime());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@implementation NapiRuntime

+ (napi_env)env {
return NativeScriptNapiEnv();
}

@end
6 changes: 6 additions & 0 deletions NativeScript/js_native_api.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Stable alias so addons can write #include <NativeScript/js_native_api.h>
// without depending on the vendored layout. Addons aiming for source
// compatibility with Node/napi-ios should prefer a bare
// #include <js_native_api.h> with Headers/napi/vendor on the header search
// path (see docs/node-api.md).
#include "napi/vendor/js_native_api.h"
106 changes: 106 additions & 0 deletions NativeScript/napi/NapiEnv.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#ifndef NapiEnv_h
#define NapiEnv_h

// Pins NAPI_VERSION for every consumer of this header, so napi_env__ is seen
// identically wherever it is compiled. Must precede the napi includes below.
#ifndef NAPI_EXPERIMENTAL
#define NAPI_EXPERIMENTAL
#endif
#ifndef NODE_API_EXPERIMENTAL_NO_WARNING
#define NODE_API_EXPERIMENTAL_NO_WARNING
#endif

#include <CoreFoundation/CoreFoundation.h>

#include <atomic>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>

#include "js_native_api_v8.h"

namespace tns {

class EventLoop;

// The finalizer of one external buffer/arraybuffer. Its callback must run
// exactly once, on the env's thread, while the env is alive — but V8's
// backing-store deleter fires on arbitrary threads, including during isolate
// disposal after the env died. So the deleter only *posts* the callback, the
// env's teardown sweep runs whatever has not run yet, and `claimed` (flipped
// exclusively on the env's thread) arbitrates between the two.
struct NapiExternalFinalizer {
std::atomic<bool> claimed{false};
napi_finalize cb = nullptr;
void* data = nullptr;
void* hint = nullptr;
};

// The napi_env behind every Node-API call, one per runtime isolate/context.
// Node's equivalent (node_napi_env__) lives in node_api.cc, which is not
// vendored; this is its replacement.
class NapiEnv : public napi_env__ {
public:
// Creates the env for `context` and hands ownership to the caller, which
// must eventually pass it to Destroy while the isolate is alive and locked.
static NapiEnv* Create(v8::Local<v8::Context> context);
static void Destroy(NapiEnv* env);

// Null when the isolate has no runtime, or before Runtime::Init reaches the
// env, or after teardown.
static NapiEnv* ForIsolate(v8::Isolate* isolate);

bool can_call_into_js() const override { return !tearingDown_; }
void CallFinalizer(napi_finalize cb, void* data, void* hint) override;
void EnqueueFinalizer(v8impl::RefTracker* finalizer) override;
void DeleteMe() override;

v8::Local<v8::Private> PrivateKey(NapiPrivateKeySlot slot);

// The runloop of the thread that owns this env — identity checks only
// (is-this-the-env's-thread); work is posted through GetEventLoop().
CFRunLoopRef RuntimeLoop() const { return runtimeLoop_; }

// The runtime's event loop, or null once ~Runtime released it. Between
// Shutdown and that release it is still returned, and posts to it are
// dropped (Post* returns false). Posted internal-lane entries run on the
// env's thread under the loop's Locker/scopes and end with a microtask
// checkpoint.
std::shared_ptr<EventLoop> GetEventLoop() const { return eventLoop_.lock(); }

// Exports of an addon already initialized in this env, or an empty handle.
v8::MaybeLocal<v8::Object> CachedModuleExports(const std::string& name);
void CacheModuleExports(const std::string& name,
v8::Local<v8::Object> exports);

// External-buffer finalizer registry, env thread only. Registered entries
// are claimed+run either by a posted backing-store deleter or by the
// teardown sweep in DeleteMe, whichever gets there first.
void RegisterExternalFinalizer(
const std::shared_ptr<NapiExternalFinalizer>& finalizer);
void RunExternalFinalizer(
const std::shared_ptr<NapiExternalFinalizer>& finalizer);

private:
explicit NapiEnv(v8::Local<v8::Context> context);
~NapiEnv() override;

void DrainFinalizers();

CFRunLoopRef runtimeLoop_ = nullptr;
std::weak_ptr<EventLoop> eventLoop_;
bool tearingDown_ = false;
v8::Eternal<v8::Private> privateKeys_[2];
std::unordered_map<std::string, v8::Global<v8::Object>> moduleExports_;
std::unordered_set<std::shared_ptr<NapiExternalFinalizer>>
externalFinalizers_;
};

// Runs the env's cleanup hooks, most recently added first, at the head of
// teardown. Defined in NodeApiEmbed.mm, next to the hook registry.
void NapiRunEnvCleanupHooks(NapiEnv* env);

} // namespace tns

#endif /* NapiEnv_h */
196 changes: 196 additions & 0 deletions NativeScript/napi/NapiEnv.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#include "NapiEnv.h"

#include <cstdio>
#include <cstdlib>

#include "NapiThreadSafeFunction.h"
#include "runtime/Helpers.h"
#include "runtime/NativeScriptException.h"
#include "runtime/Runtime.h"

using namespace v8;

namespace tns {

NapiEnv::NapiEnv(Local<Context> context)
: napi_env__(context, NODE_API_DEFAULT_MODULE_API_VERSION),
runtimeLoop_(CFRunLoopGetCurrent()),
eventLoop_(Runtime::GetCurrentRuntime()->GetEventLoop()) {}

NapiEnv::~NapiEnv() = default;

NapiEnv* NapiEnv::Create(Local<Context> context) { return new NapiEnv(context); }

void NapiEnv::Destroy(NapiEnv* env) {
if (env == nullptr) {
return;
}

// Drops the reference taken at construction, which runs the finalizer drain
// and deletes the env.
env->Unref();
}

NapiEnv* NapiEnv::ForIsolate(Isolate* isolate) {
if (isolate == nullptr) {
return nullptr;
}

Runtime* runtime = Runtime::GetRuntime(isolate);
if (runtime == nullptr) {
return nullptr;
}

return static_cast<NapiEnv*>(runtime->GetNapiEnv());
}

void NapiEnv::CallFinalizer(napi_finalize cb, void* data, void* hint) {
if (cb == nullptr) {
return;
}

HandleScope handle_scope(this->isolate);
Context::Scope context_scope(this->context());

CallIntoModule([&](napi_env env) { cb(env, data, hint); },
[](napi_env env, Local<Value> exception) {
if (env->terminatedOrTerminating()) {
return;
}
NativeScriptException::ReportToJsHandlersAndLog(
env->isolate, exception, Local<Message>());
});
}

void NapiEnv::EnqueueFinalizer(v8impl::RefTracker* finalizer) {
// Runs inside V8's weak callback, where calling into JS is forbidden. The
// queue is drained on a later event-loop entry instead, which is where Node
// puts it too (a SetImmediate there, an internal-lane post here). One drain
// is scheduled per non-empty stretch of the queue.
bool scheduled = !this->pending_finalizers.empty();
napi_env__::EnqueueFinalizer(finalizer);

if (scheduled || this->tearingDown_) {
return;
}

std::shared_ptr<tns::EventLoop> loop = this->GetEventLoop();
if (loop == nullptr) {
return;
}

// The entry runs under the loop's Locker/scopes; EventLoop::Shutdown drops
// queued entries before ~Runtime destroys this env, so `this` is live here.
NapiEnv* env = this;
loop->PostInternal([env]() { env->DrainFinalizers(); });
}

void NapiEnv::RegisterExternalFinalizer(const std::shared_ptr<NapiExternalFinalizer>& finalizer) {
this->externalFinalizers_.insert(finalizer);
}

void NapiEnv::RunExternalFinalizer(const std::shared_ptr<NapiExternalFinalizer>& finalizer) {
if (finalizer->claimed.exchange(true)) {
return;
}

this->CallFinalizer(finalizer->cb, finalizer->data, finalizer->hint);
this->externalFinalizers_.erase(finalizer);
}

void NapiEnv::DrainFinalizers() {
while (!this->pending_finalizers.empty()) {
v8impl::RefTracker* finalizer = *this->pending_finalizers.begin();
this->pending_finalizers.erase(finalizer);
finalizer->Finalize();
}
}

void NapiEnv::DeleteMe() {
// ~Runtime holds the Locker but never enters the isolate (same situation
// ObjectManager::DisposeAllRegistered handles), so teardown enters it here
// before anything below touches handles or the context.
Isolate::Scope isolate_scope(this->isolate);
HandleScope handle_scope(this->isolate);

// From here on can_call_into_js() is false: hooks and finalizers still run
// and may release env-bound resources (delete refs, release threadsafe
// functions), but any Node-API call that would enter JS is refused, matching
// Node's teardown contract.
this->tearingDown_ = true;

// Cleanup hooks come first, so an addon gets to release its threadsafe
// functions and other env-bound resources itself. Whatever survives is
// closed below, before any reference is finalized — a threadsafe function
// holds one to its JS callback.
NapiRunEnvCleanupHooks(this);
NapiAbortThreadSafeFunctions(this);

this->DrainFinalizers();

v8impl::RefTracker::FinalizeAll(&this->finalizing_reflist);
v8impl::RefTracker::FinalizeAll(&this->reflist);

// External-buffer finalizers whose backing-store deleter has not fired (or
// whose posted run was dropped by Shutdown) run here, while the env can
// still make the callback; the deleter finds them claimed and does nothing.
while (!this->externalFinalizers_.empty()) {
this->RunExternalFinalizer(*this->externalFinalizers_.begin());
}

this->moduleExports_.clear();

delete this;
}

Local<Private> NapiEnv::PrivateKey(NapiPrivateKeySlot slot) {
size_t index = static_cast<size_t>(slot);
if (this->privateKeys_[index].IsEmpty()) {
const char* name = slot == NapiPrivateKeySlot::wrapper
? "node_api.wrapper"
: "node_api.type_tag";
Local<Private> key =
Private::New(this->isolate, tns::ToV8String(this->isolate, name));
this->privateKeys_[index].Set(this->isolate, key);
}

return this->privateKeys_[index].Get(this->isolate);
}

MaybeLocal<Object> NapiEnv::CachedModuleExports(const std::string& name) {
auto it = this->moduleExports_.find(name);
if (it == this->moduleExports_.end()) {
return MaybeLocal<Object>();
}

return it->second.Get(this->isolate);
}

void NapiEnv::CacheModuleExports(const std::string& name,
Local<Object> exports) {
this->moduleExports_[name].Reset(this->isolate, exports);
}

Local<Private> NapiPrivateKey(Local<Context> context, NapiPrivateKeySlot slot) {
// The context argument exists to match upstream's macro; one env per isolate
// makes it redundant, and V8 no longer exposes Context::GetIsolate.
(void)context;
Isolate* isolate = Isolate::GetCurrent();
NapiEnv* env = NapiEnv::ForIsolate(isolate);
tns::Assert(env != nullptr, isolate,
"Node-API private key requested without a napi_env");
return env->PrivateKey(slot);
}

} // namespace tns

namespace v8impl {

void OnFatalError(const char* location, const char* message) {
Log(@"NativeScript Node-API fatal error: %s%s%s", message,
location != nullptr ? " at " : "", location != nullptr ? location : "");
tns::LogBacktrace();
abort();
}

} // namespace v8impl
26 changes: 26 additions & 0 deletions NativeScript/napi/NapiModules.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifndef NapiModules_h
#define NapiModules_h

#include <string>

#include "runtime/Common.h"

namespace tns {

// The process-global table of Node-API addons registered through
// napi_module_register, and their per-env instantiation. Shaped after
// NsBuiltinModules so require() can consult both the same way.
class NapiModules {
public:
static bool IsRegistered(const std::string& name);

// Returns the addon's exports for the context's env, initializing it on
// first use and reusing that object afterwards. Empty on failure, with the
// exception left pending on the isolate.
static v8::MaybeLocal<v8::Object> GetExports(v8::Local<v8::Context> context,
const std::string& name);
};

} // namespace tns

#endif /* NapiModules_h */
Loading
Loading