Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/social-parks-take.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-native-node-api": minor
---

Provided implementation of napi_async_init, napi_async_destroy, napi_make_callback
156 changes: 89 additions & 67 deletions packages/host/cpp/RuntimeNodeApiAsync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,74 @@
#include <ReactCommon/CallInvoker.h>
#include "Logger.hpp"

struct AsyncJob {
using IdType = uint64_t;
enum State { Created, Queued, Completed, Cancelled, Deleted };
using IdType = uint64_t;

struct AsyncContext {
IdType id{};
State state{};
napi_env env;
napi_value async_resource;
napi_value async_resource_name;
napi_async_execute_callback execute;
napi_async_complete_callback complete;
void* data{nullptr};

static AsyncJob* fromWork(napi_async_work work) {
return reinterpret_cast<AsyncJob*>(work);
}
static napi_async_work toWork(AsyncJob* job) {
return reinterpret_cast<napi_async_work>(job);
}
AsyncContext(
napi_env env, napi_value async_resource, napi_value async_resource_name)
: env{env},
async_resource{async_resource},
async_resource_name{async_resource_name} {}
};

class AsyncWorkRegistry {
public:
using IdType = AsyncJob::IdType;
struct AsyncJob : AsyncContext {
enum State { Created, Queued, Completed, Cancelled, Deleted };

std::shared_ptr<AsyncJob> create(napi_env env,
State state{State::Created};
napi_async_execute_callback execute;
napi_async_complete_callback complete;
void* data{nullptr};

AsyncJob(napi_env env,
napi_value async_resource,
napi_value async_resource_name,
napi_async_execute_callback execute,
napi_async_complete_callback complete,
void* data) {
const auto job = std::shared_ptr<AsyncJob>(new AsyncJob{
.id = next_id(),
.state = AsyncJob::State::Created,
.env = env,
.async_resource = async_resource,
.async_resource_name = async_resource_name,
.execute = execute,
.complete = complete,
.data = data,
});

jobs_[job->id] = job;
return job;
}
void* data)
: AsyncContext{env, async_resource, async_resource_name},
execute{execute},
complete{complete},
data{data} {}
};

std::shared_ptr<AsyncJob> get(napi_async_work work) const {
const auto job = AsyncJob::fromWork(work);
if (!job) {
return {};
}
if (const auto it = jobs_.find(job->id); it != jobs_.end()) {
return it->second;
}
return {};
template <class T, class U>
class Container {
public:
void push(std::shared_ptr<T>&& obj) {
const auto id = nextId();
obj->id = id;
map_[id] = std::move(obj);
}

bool release(IdType id) {
if (const auto it = jobs_.find(id); it != jobs_.end()) {
it->second->state = AsyncJob::State::Deleted;
jobs_.erase(it);
return true;
}
return false;
std::shared_ptr<T> get(const U obj) {
return map_.contains(id(obj)) ? map_[id(obj)] : nullptr;
}
bool release(const U obj) { return map_.erase(id(obj)) > 0; }

private:
IdType next_id() {
if (current_id_ == std::numeric_limits<IdType>::max()) [[unlikely]] {
current_id_ = 0;
IdType id(const U obj) const {
auto casted = reinterpret_cast<T*>(obj);
return casted ? casted->id : 0;
}
IdType nextId() {
if (currentId_ == std::numeric_limits<IdType>::max()) [[unlikely]] {
currentId_ = 0;
}
return ++current_id_;
return ++currentId_;
}

IdType current_id_{0};
std::unordered_map<IdType, std::shared_ptr<AsyncJob>> jobs_;
IdType currentId_{0};
std::unordered_map<IdType, std::shared_ptr<T>> map_;
};

static std::unordered_map<napi_env, std::weak_ptr<facebook::react::CallInvoker>>
callInvokers;
static AsyncWorkRegistry asyncWorkRegistry;
static Container<AsyncJob, napi_async_work> jobs_;
static Container<AsyncContext, napi_async_context> contexts_;

namespace callstack::nodeapihost {

Expand All @@ -104,20 +91,16 @@ napi_status napi_create_async_work(napi_env env,
napi_async_complete_callback complete,
void* data,
napi_async_work* result) {
const auto job = asyncWorkRegistry.create(
auto job = std::make_shared<AsyncJob>(
env, async_resource, async_resource_name, execute, complete, data);
if (!job) {
log_debug("Error: Failed to create async work job");
return napi_generic_failure;
}

*result = AsyncJob::toWork(job.get());
*result = reinterpret_cast<napi_async_work>(job.get());
jobs_.push(std::move(job));
return napi_ok;
}

napi_status napi_queue_async_work(
node_api_basic_env env, napi_async_work work) {
const auto job = asyncWorkRegistry.get(work);
const auto job = jobs_.get(work);
if (!job) {
log_debug("Error: Received null job in napi_queue_async_work");
return napi_invalid_arg;
Expand Down Expand Up @@ -151,13 +134,14 @@ napi_status napi_queue_async_work(

napi_status napi_delete_async_work(
node_api_basic_env env, napi_async_work work) {
const auto job = asyncWorkRegistry.get(work);
const auto job = jobs_.get(work);
if (!job) {
log_debug("Error: Received non-existent job in napi_delete_async_work");
return napi_invalid_arg;
}

if (!asyncWorkRegistry.release(job->id)) {
job->state = AsyncJob::State::Deleted;
if (!jobs_.release(work)) {
log_debug("Error: Failed to release async work job");
return napi_generic_failure;
}
Expand All @@ -167,7 +151,7 @@ napi_status napi_delete_async_work(

napi_status napi_cancel_async_work(
node_api_basic_env env, napi_async_work work) {
const auto job = asyncWorkRegistry.get(work);
const auto job = jobs_.get(work);
if (!job) {
log_debug("Error: Received null job in napi_cancel_async_work");
return napi_invalid_arg;
Expand All @@ -187,4 +171,42 @@ napi_status napi_cancel_async_work(
job->state = AsyncJob::State::Cancelled;
return napi_ok;
}

napi_status napi_async_init(napi_env env,
napi_value async_resource,
napi_value async_resource_name,
napi_async_context* result) {
if (!result) {
return napi_invalid_arg;
}
auto context =
std::make_shared<AsyncContext>(env, async_resource, async_resource_name);
*result = reinterpret_cast<napi_async_context>(context.get());
contexts_.push(std::move(context));
return napi_ok;
}

napi_status napi_async_destroy(napi_env env, napi_async_context async_context) {
if (!async_context) {
return napi_invalid_arg;
}
if (!contexts_.release(async_context)) {
return napi_generic_failure;
}
return napi_ok;
}

napi_status napi_make_callback(napi_env env,
napi_async_context async_context,
napi_value recv,
napi_value func,
size_t argc,
const napi_value* argv,
napi_value* result) {
const auto status = napi_call_function(env, recv, func, argc, argv, result);
if (status == napi_pending_exception && async_context) {
contexts_.release(async_context);
}
return status;
}
} // namespace callstack::nodeapihost
16 changes: 16 additions & 0 deletions packages/host/cpp/RuntimeNodeApiAsync.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,20 @@ napi_status napi_delete_async_work(

napi_status napi_cancel_async_work(
node_api_basic_env env, napi_async_work work);

napi_status napi_async_init(napi_env env,
napi_value async_resource,
napi_value async_resource_name,
napi_async_context* result);

napi_status napi_async_destroy(napi_env env, napi_async_context async_context);

napi_status napi_make_callback(napi_env env,
napi_async_context async_context,
napi_value recv,
napi_value func,
size_t argc,
const napi_value* argv,
napi_value* result);

} // namespace callstack::nodeapihost
3 changes: 3 additions & 0 deletions packages/host/scripts/generate-weak-node-api-injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [
"napi_fatal_error",
"napi_get_node_version",
"napi_get_version",
"napi_async_init",
"napi_async_destroy",
"napi_make_callback",
];

/**
Expand Down
5 changes: 2 additions & 3 deletions packages/node-addon-examples/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
cb();
if (expectedMessages.length > 0) {
errors.push(
new Error(
`Missing expected message(s): ${expectedMessages.join(", ")}`,
),
new Error(`Missing expected message(s): ${expectedMessages.join(", ")}`)
);
}
} finally {
Expand Down Expand Up @@ -85,5 +83,6 @@
require("../tests/buffers/addon.js");
},
async: () => require("../tests/async/addon.js") as () => Promise<void>,
make_callback: () => require("../tests/make_callback/addon.js"),

Check failure on line 86 in packages/node-addon-examples/src/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe return of a value of type `any`
},
};
15 changes: 15 additions & 0 deletions packages/node-addon-examples/tests/make_callback/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.15)
project(tests-make_callback)

add_compile_definitions(NAPI_VERSION=8)

add_library(addon SHARED addon.c ${CMAKE_JS_SRC})
set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node")
target_include_directories(addon PRIVATE ${CMAKE_JS_INC})
target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB})
target_compile_features(addon PRIVATE cxx_std_17)

if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
# Generate node.lib
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
endif()
68 changes: 68 additions & 0 deletions packages/node-addon-examples/tests/make_callback/addon.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <assert.h>
#include <node_api.h>
#include <stdio.h>
#include "../RuntimeNodeApiTestsCommon.h"

#define MAX_ARGUMENTS 10
#define RESERVED_ARGS 3

static napi_value MakeCallback(napi_env env, napi_callback_info info) {
size_t argc = MAX_ARGUMENTS;
size_t n;
napi_value args[MAX_ARGUMENTS];
// NOLINTNEXTLINE (readability/null_usage)
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));

NODE_API_ASSERT(env, argc > 0, "Wrong number of arguments");
printf("Make callback called with %zu arguments\n", argc);

napi_value resource = args[0];
napi_value recv = args[1];
napi_value func = args[2];

napi_value argv[MAX_ARGUMENTS - RESERVED_ARGS];
for (n = RESERVED_ARGS; n < argc; n += 1) {
argv[n - RESERVED_ARGS] = args[n];
}

napi_valuetype func_type;

NODE_API_CALL(env, napi_typeof(env, func, &func_type));

napi_value resource_name;
NODE_API_CALL(env,
napi_create_string_utf8(env, "test", NAPI_AUTO_LENGTH, &resource_name));

napi_async_context context;
NODE_API_CALL(env, napi_async_init(env, resource, resource_name, &context));

napi_value result;
if (func_type == napi_function) {
NODE_API_CALL(env,
napi_make_callback(
env, context, recv, func, argc - RESERVED_ARGS, argv, &result));
} else {
NODE_API_ASSERT(env, false, "Unexpected argument type");
}

NODE_API_CALL(env, napi_async_destroy(env, context));

return result;
}

static napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
NODE_API_CALL(env,
napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env,
NULL,
NAPI_AUTO_LENGTH,
MakeCallback,
NULL,
&fn));
NODE_API_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn));
return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
Loading
Loading