Skip to content
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

Add node.js bindings #67

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.idea/
build
.vscode
node_modules
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ option(ENABLE_EXAMPLES "Enable examples" OFF)
option(ENABLE_DOC "Build documentation" ON)
option(ENABLE_CLIENT "Enable client application for parsing HawkTracer stream" ON)
option(ENABLE_PYTHON_BINDINGS "Enable Python bindings (requires ENABLE_CLIENT=ON)" OFF)
option(ENABLE_NODEJS_BINDINGS "Enable Node.js bindings (requires ENABLE_CLIENT=ON)" OFF)

option(ENABLE_POSITION_INDEPENDENT_CODE "Enable position independent code of
the HawkTracer library. For most of the casses, it adds -fPIC flag to a compiler." OFF)
Expand Down
4 changes: 4 additions & 0 deletions bindings/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
if(ENABLE_PYTHON_BINDINGS)
add_subdirectory(python3)
endif()

if(ENABLE_NODEJS_BINDINGS)
add_subdirectory(nodejs)
endif()
43 changes: 43 additions & 0 deletions bindings/nodejs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# https://www.npmjs.com/package/cmake-js#general
if (CMAKE_JS_SRC)
beila marked this conversation as resolved.
Show resolved Hide resolved
add_library(cmake_js STATIC ${CMAKE_JS_SRC})
else()
add_library(cmake_js INTERFACE)
endif()
target_include_directories(cmake_js INTERFACE SYSTEM ${CMAKE_JS_INC})
target_link_libraries(cmake_js ${CMAKE_JS_LIB})

# https://www.npmjs.com/package/cmake-js#n-api-and-node-addon-api
add_library(node_addon_api INTERFACE)
if (NOT NODE_RUNTIME)
find_program(NODE_RUNTIME NAMES node HINTS /usr)
endif()
execute_process(COMMAND ${NODE_RUNTIME} -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
if (NOT NODE_ADDON_API_DIR)
message(FATAL_ERROR "Failed to find node-addon-api headers with node command \"${NODE_RUNTIME}\"")
endif()
string(REPLACE "\n" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
string(REPLACE "\"" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
target_include_directories(node_addon_api INTERFACE SYSTEM ${NODE_ADDON_API_DIR})
target_link_libraries(node_addon_api INTERFACE cmake_js)

# Supports Node version >= 12.11.0 https://nodejs.org/api/n-api.html#n_api_n_api_version_matrix
target_compile_definitions(node_addon_api INTERFACE NAPI_VERSION=5)

if (ENABLE_CLIENT)
beila marked this conversation as resolved.
Show resolved Hide resolved
add_library(nodejs_bindings_client SHARED
hawktracer_client_nodejs.cpp
hawktracer_client_nodejs.hpp
client_context.cpp
client_context.hpp)
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawk_tracer_client SUFFIX .node)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawk_tracer_client SUFFIX .node)
set_target_properties(nodejs_bindings_client PROPERTIES PREFIX "" OUTPUT_NAME hawktracer_client SUFFIX .node)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project name is HawkTracer in README.md. Isn't it two words?

target_link_libraries(nodejs_bindings_client PUBLIC node_addon_api hawktracer_parser hawktracer_client_utils)

# cmake-js specifies CMAKE_LIBRARY_OUTPUT_DIRECTORY.
add_custom_target(make_directory_for_nodejs_bindings_client
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
add_dependencies(nodejs_bindings_client make_directory_for_nodejs_bindings_client)
endif ()
64 changes: 64 additions & 0 deletions bindings/nodejs/client_context.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "client_context.hpp"

#include "hawktracer/client_utils/stream_factory.hpp"
#include "hawktracer/parser/make_unique.hpp"
#include <iostream>
#include <utility>

namespace HawkTracer
{
namespace Nodejs
{

std::unique_ptr<ClientContext> ClientContext::create(const std::string &source, EventCallback event_callback)
{
std::unique_ptr<parser::Stream> stream = ClientUtils::make_stream_from_string(source);
if (!stream) {
return nullptr;
beila marked this conversation as resolved.
Show resolved Hide resolved
}

std::unique_ptr<parser::KlassRegister> klass_register{new parser::KlassRegister()};
beila marked this conversation as resolved.
Show resolved Hide resolved
std::unique_ptr<parser::ProtocolReader>
reader{new parser::ProtocolReader(klass_register.get(), std::move(stream), true)};
std::unique_ptr<ClientContext>
context{new ClientContext(std::move(reader), std::move(klass_register), std::move(event_callback))};
return context;
}

ClientContext::ClientContext(std::unique_ptr<parser::ProtocolReader> reader,
std::unique_ptr<parser::KlassRegister> klass_register,
EventCallback event_callback)
: _reader(std::move(reader)), _klass_register(std::move(klass_register)), _event_callback(std::move(event_callback))
{
_reader->register_events_listener(
[this](const parser::Event &event)
{
if (!_events) {
_events.reset(new std::vector<parser::Event>{});
}
_events->push_back(event);
_events = _event_callback(std::move(_events), ConsumeMode::TRY_CONSUME);
});
_reader->register_complete_listener(
[this]()
{
if (!_events || _events->empty()) {
return;
}
_events = _event_callback(std::move(_events), ConsumeMode::FORCE_CONSUME);
if (_events && !_events->empty()) {
std::cerr << _events->size() << " events were not processed." << std::endl;
}
});

_reader->start();
}

ClientContext::~ClientContext()
{
_reader->stop();
}

} // namespace Nodejs
} // namespace HawkTracer

43 changes: 43 additions & 0 deletions bindings/nodejs/client_context.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#ifndef CLIENT_CONTEXT_HPP
#define CLIENT_CONTEXT_HPP

#include <string>

#include "hawktracer/parser/event.hpp"
#include "hawktracer/parser/protocol_reader.hpp"
#include "hawktracer/parser/klass_register.hpp"

namespace HawkTracer
{
namespace Nodejs
{

class ClientContext
{
public:
enum class ConsumeMode
{
TRY_CONSUME, FORCE_CONSUME
};
using EventCallback = std::function<std::unique_ptr<std::vector<parser::Event>>(std::unique_ptr<std::vector<parser::Event>>,
ConsumeMode)>;
static std::unique_ptr<ClientContext> create(const std::string &source, EventCallback event_callback);

~ClientContext();

private:
ClientContext(std::unique_ptr<parser::ProtocolReader> reader,
std::unique_ptr<parser::KlassRegister> klass_register,
EventCallback event_callback);

const std::unique_ptr<parser::ProtocolReader> _reader;
const std::unique_ptr<const parser::KlassRegister> _klass_register;
const EventCallback _event_callback;

std::unique_ptr<std::vector<parser::Event>> _events;
beila marked this conversation as resolved.
Show resolved Hide resolved
beila marked this conversation as resolved.
Show resolved Hide resolved
};

} // namespace Nodejs
} // namespace HawkTracer

#endif //CLIENT_CONTEXT_HPP
185 changes: 185 additions & 0 deletions bindings/nodejs/hawktracer_client_nodejs.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#include "hawktracer_client_nodejs.hpp"

#include <iostream>
#include <utility>

namespace HawkTracer
{
namespace Nodejs
{

Object Client::init_bindings(class Env env, Object exports)
beila marked this conversation as resolved.
Show resolved Hide resolved
{
HandleScope scope(env);

Function constructor = DefineClass(
env,
"HawkTracerClient",
{
InstanceMethod("start", &Client::start),
InstanceMethod("onEvents", &Client::set_on_events),
InstanceMethod("stop", &Client::stop)
});
Persistent(constructor).SuppressDestruct();
exports.Set("HawkTracerClient", constructor);
return exports;
}

Client::Client(const CallbackInfo &info)
: ObjectWrap<Client>(info)
{
_source = info[0].As<String>();
}

Client::~Client()
{
_stop();
}

Value Client::start(const CallbackInfo &info)
beila marked this conversation as resolved.
Show resolved Hide resolved
{
_context_holder->context = ClientContext::create(
_source,
[this](std::unique_ptr<std::vector<parser::Event>> data, ClientContext::ConsumeMode consume_mode)
{
return handle_events(std::move(data), consume_mode);
});
return Boolean::New(info.Env(), static_cast<bool>(_context_holder->context));
beila marked this conversation as resolved.
Show resolved Hide resolved
}

void Client::stop(const CallbackInfo &)
beila marked this conversation as resolved.
Show resolved Hide resolved
{
_stop();
}

void Client::_stop()
{
std::lock_guard<std::mutex> lock{_callback_mutex};
if (_callback) {
_callback->function.Abort();
_callback->function.Release();
_callback.reset();
}
else {
delete _context_holder;
}
}

void Client::set_on_events(const CallbackInfo &info)
{
std::lock_guard<std::mutex> lock{_callback_mutex};
if (_callback) {
// existing _context_holder will be deleted by the finalizer of existing _callback.function
_context_holder = new ContextHolder{std::move(*_context_holder)};
_callback->function.Release();
}
_callback.reset(new ThreadSafeFunctionHolder{
ThreadSafeFunction::New(info.Env(),
info[0].As<Napi::Function>(),
"HawkTracerClientOnEvent",
1,
1,
[](class Env, decltype(_context_holder) context_holder)
{
delete context_holder;
},
_context_holder)});
}

// This method is called from reader thread, while all other methods are called from js main thread.
std::unique_ptr<std::vector<parser::Event>>
Client::handle_events(std::unique_ptr<std::vector<parser::Event>> events, ClientContext::ConsumeMode consume_mode)
{
std::lock_guard<std::mutex> lock{_callback_mutex};
if (!_callback) {
return events;
beila marked this conversation as resolved.
Show resolved Hide resolved
}

// deallocated in convert_and_callback() or below in this method
auto data = new CallbackDataType{this, std::move(events)};
napi_status status;
if (consume_mode == ClientContext::ConsumeMode::FORCE_CONSUME) {
status = _callback->function.BlockingCall(data, &Client::convert_and_callback);
}
else {
status = _callback->function.NonBlockingCall(data, &Client::convert_and_callback);
}

decltype(events) ret{};
if (status == napi_queue_full) {
ret = std::move(data->second);
}
if (status != napi_ok && status != napi_queue_full) {
std::cerr << "Request for callback failed with error code: " << status << ", " << data->second->size()
<< " events are lost." << std::endl;
}
if (status != napi_ok) {
delete data;
}
return ret;
}

Value Client::convert_field_value(class Env env, const parser::Event::Value &value)
{
switch (value.field->get_type_id()) {
case parser::FieldTypeId::UINT8:
return Number::New(env, value.value.f_UINT8);
case parser::FieldTypeId::INT8:
return Number::New(env, value.value.f_INT8);
case parser::FieldTypeId::UINT16:
return Number::New(env, value.value.f_UINT16);
case parser::FieldTypeId::INT16:
return Number::New(env, value.value.f_INT16);
case parser::FieldTypeId::UINT32:
return Number::New(env, value.value.f_UINT32);
case parser::FieldTypeId::INT32:
return Number::New(env, value.value.f_INT32);
case parser::FieldTypeId::UINT64:
return Number::New(env, value.value.f_UINT64);
case parser::FieldTypeId::INT64:
return Number::New(env, value.value.f_INT64);
case parser::FieldTypeId::STRING:
return String::New(env, value.value.f_STRING);
case parser::FieldTypeId::POINTER:
return String::New(env, "(pointer)");
case parser::FieldTypeId::STRUCT:
return convert_event(env, *value.value.f_EVENT);
default:
assert(0);
}
}

Object Client::convert_event(class Env env, const parser::Event &event)
{
auto o = Object::New(env);
for (const auto &it: event.get_values()) {
o.Set(it.first, convert_field_value(env, it.second));
}
return o;
}

void Client::convert_and_callback(class Env env, Function real_callback, CallbackDataType *data)
{
std::unique_ptr<CallbackDataType> data_deallocation_guard{data};
Client *calling_object = data->first;
beila marked this conversation as resolved.
Show resolved Hide resolved
std::vector<parser::Event> *events = data->second.get();
beila marked this conversation as resolved.
Show resolved Hide resolved

// Prevent Client destruction, which could result in blocking call in handle_events(), which is blocked by
// real_callback running in js thread, forming deadlock.
calling_object->Ref();

Array array = Array::New(env);
int i = 0;
std::for_each(events->cbegin(),
events->cend(),
[env, &array, &i](const parser::Event &e)
{
array[i++] = convert_event(env, e);
});
real_callback.Call({array});

calling_object->Unref();
}

} // namespace Nodejs
} // namespace HawkTracer
Loading