Skip to content

Implementation of napi_async_init, napi_async_destroy, napi_make_callback #199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
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": patch
---

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
1 change: 1 addition & 0 deletions packages/node-addon-examples/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,6 @@ export const suites: Record<
tests: {
buffers: () => require("../tests/buffers/addon.js"),
async: () => require("../tests/async/addon.js"),
make_callback: () => require("../tests/make_callback/addon.js"),
},
};
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