Node-addon-api: How to free a malloc'd pointer passed through node? (Finalizer with External)

Created on 4 Feb 2020  路  6Comments  路  Source: nodejs/node-addon-api

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!

Most helpful comment

@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.

All 6 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

langxiong picture langxiong  路  5Comments

sziraqui picture sziraqui  路  5Comments

D-Andreev picture D-Andreev  路  6Comments

bmacnaughton picture bmacnaughton  路  6Comments

am2222 picture am2222  路  8Comments