I'm wrapping a C++ lib to be used in node.
I need to pass something (struct, struct's pointer ...) through node. I use malloc (also new) for that.
Napi::Value allocint(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
Napi::Object obj = Napi::Object::New(env);
int* test;
// test = new int;
test = static_cast<int *>(malloc(sizeof(int)));
*test = 0xC;
printf(" OPEN \n");
printf(" test 0x%X\n", test);
printf("*test 0x%X\n", *test);
printf("&test 0x%X\n", &test);
return Napi::External<int*>::New(info.Env(), &test);
}
Napi::Value freeint(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
int* test = *info[0].As<Napi::External<int*>>().Data();
printf(" CLOSE \n");
printf(" test 0x%X\n", test);
printf("*test 0x%X\n", *test);
printf("&test 0x%X\n", &test);
// delete test;
free(test);
}
const app = require('bindings')('myapp');
let test = app.open(); // test should be the External pointer
app.close(test);
I get a Segmentation fault caused because I don't use the initial test pointer with free but a fake pointer with the right address.
test 0x3EA92E0
*test 0xC
&test 0x3D625EB0
CLOSE
test 0x3EA92E0
*test 0xC
&test 0x3D625ED0
Segmentation fault (core dumped)
It seems External::New doesn't keep the C++ datatype. Of course, if I pass the C++ variable without using malloc/free, the pointer gets vanished when the function returns.
How can I keep a pointer/struct/c++ element in node? I'd like to avoid using global variable.
Thanks!
Hi @alexisfrjp ,
You can use an External to wrap T* via External<T>. You can add a Finalizer callback to External::New that will run when V8 garbage-collects your object, where you would want to free it.
Let me know if this helps
@alexisfrjp The code below is not tested, but something like this should work:
to create:
int* test = new int;
Napi::External<int> js_test = Napi::External::New(info.Env(), test);
js_test.As<Napi::Object>().AddFinalizer([](Napi::Env env, int* data) {
delete data;
});
return js_test;
to use:
static void SomeBinding(const Napi::CallbackInfo& info) {
int* test = info[0].As<Napi::External<int>>().Data();
*test = 5;
}
Hi @KevinEady, @gabrielschulhof
Thanks for your suggestion and your example.
It seems that adding the extra line to use AddFinalizer breaks my code.
Napi::Value sendintptr(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
int* test;
test = new int;
*test = 0xC;
printf(" OPEN \n");
printf(" test 0x%10X *test 0x%10X &test 0x%10X\n", test, *test, &test);
Napi::External<int*> external = Napi::External<int*>::New(env, &test);
external.As<Napi::Object>()
.AddFinalizer([](Napi::Env env, const int* data) {
printf("finalizeCallback happens!\n");
delete data;
}, test);
return external;
}
Napi::Value getintptr(const Napi::CallbackInfo &info) {
int* test = *info[0].As<Napi::External<int*>>().Data();
printf(" CLOSE \n");
printf(" test 0x%10X *test 0x%10X &test 0x%10X\n", test, *test, &test);
}
In Node:
let externalintptr = addon.sendintptr();
addon.getintptr(externalintptr);
Before:
test 0x 3203790 *test 0x C &test 0x 3DC40740
CLOSE
test 0x 3203790 *test 0x C &test 0x 3DC40760
After:
test 0x 3E80780 *test 0x C &test 0x 4D69CE30
CLOSE
test 0x 4D69CEA0 *test 0x 4D69CFA0 &test 0x 4D69CE70
finalizeCallback happens!
Somehow AddFinalizer breaks something.
Hi @alexisfrjp ,
If you are creating new int, your External needs to be External<int>, and performing ext.Data() will return you an int*. Does this help?
Maybe take a look at https://github.com/nodejs/node-addon-api/blob/master/test/external.cc
@alexisfrjp here's a complete, tested example:
#include <stdio.h>
#define NAPI_EXPERIMENTAL
#include "napi.h"
static Napi::Value CreateInt(const Napi::CallbackInfo& info) {
int* test = new int(0xc);
printf(" OPEN \n");
printf(" test %p *test %x &test %p\n", test, *test, &test);
Napi::External<int> js_test = Napi::External<int>::New(info.Env(), test);
js_test.As<Napi::Object>().AddFinalizer([](Napi::Env, int* test) {
printf(" finalize \n"
" test %p "
"*test %x "
"&test %p\n", test, *test, &test);
}, test);
return js_test;
}
static void ConsumeInt(const Napi::CallbackInfo& info) {
int* test = info[0].As<Napi::External<int>>().Data();
printf(" CLOSE \n");
printf(" test %p *test %x &test %p\n", test, *test, &test);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports["consumeInt"] = Napi::Function::New(env, ConsumeInt);
exports["createInt"] = Napi::Function::New(env, CreateInt);
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
const binding = require('./build/Release/test');
let int = binding.createInt();
binding.consumeInt(int);
In the latest version of Node.js 12.x the finalizer will run on exit. In earlier versions, if you would like to see the finalizer run you have to run Node.js with --expose-gc and add
int = null;
global.gc();
to the JS.
@KevinEady @gabrielschulhof Sorry for the late reply. Thank you very much, that worked perfectly!
I mistranslated my case from the example https://github.com/nodejs/node-addon-api/issues/584#issuecomment-548936090 that gives the address of the value not the value itself to Napi::External<>::New.
Most helpful comment
@alexisfrjp here's a complete, tested example:
In the latest version of Node.js 12.x the finalizer will run on exit. In earlier versions, if you would like to see the finalizer run you have to run Node.js with
--expose-gcand addto the JS.