Koromix / koffi

Fast and easy-to-use C FFI module for Node.js
https://koffi.dev/
MIT License
170 stars 3 forks source link

[koffi] FATAL ERROR: Error::ThrowAsJavaScriptException napi_throw #42

Closed wanglin86769 closed 1 year ago

wanglin86769 commented 1 year ago

Hello, if I add callback as a function argument, the node.js will crash and the callback does not execute. Am I doing something wrong? Is there any way to troubleshoot this problem? Thanks.

In the following code, if callback is added to ca_create_subscription(), node.js will crash.

Node.js code

const koffi = require('koffi');

let LIBCA_PATH = '.\\clibs\\win64\\ca.dll';
const libca = koffi.load(LIBCA_PATH);

let pointer = koffi.pointer('pointer', koffi.opaque(), 2);
let chanId = koffi.pointer('chanId', koffi.opaque());

const evargs_t = koffi.struct('evargs_t', {
  usr: 'void *',
  chid: chanId,
  type: 'long',
  count: 'long',
  dbr: 'void *',
  status: 'int'
});
const MonitorCallback = koffi.callback('MonitorCallback', 'void', [evargs_t]);
let callback = koffi.register(args => {
    console.log('*** Entering cb1 ***');
}, koffi.pointer(MonitorCallback));

const ca_context_create = libca.func('ca_context_create', 'int', ['int']);
const ca_pend_event = libca.func('ca_pend_event', 'int', ['double']);
const ca_pend_io = libca.func('ca_pend_io', 'int', ['double']);
const ca_create_channel = libca.func('ca_create_channel', 'int', ['string','pointer','pointer','int',koffi.out(pointer)]);
const ca_field_type = libca.func('ca_field_type', 'short', [chanId]);
const ca_element_count = libca.func('ca_element_count', 'int', [chanId]);
const ca_create_subscription = libca.func('ca_create_subscription', 'int', ['int','ulong',chanId,'long',koffi.pointer(MonitorCallback),'pointer','pointer']);

let chidPtr = [null];
let priority = 0;
ca_context_create(1);
ca_create_channel('RTBT_Diag:RTBPM01:x1', null, null, priority, chidPtr);
ca_pend_io(1.0);
let chid = chidPtr[0];
fieldType = ca_field_type(chidPtr[0]);
count = ca_element_count(chidPtr[0]);
console.log(fieldType);
console.log(count);

console.log('*** Before callback ***');
ca_create_subscription(fieldType, count, chid, 1, callback, null, null);
console.log('*** After callback ***');
ca_pend_io(1.0);
ca_pend_event(1.0);

Output:

6
1
*** Before callback ***
*** After callback ***
FATAL ERROR: Error::ThrowAsJavaScriptException napi_throw
 1: 00007FF63256151F v8::internal::CodeObjectRegistry::~CodeObjectRegistry+121999
 2: 00007FF6324EB386 DSA_meth_get_flags+64118
 3: 00007FF6324EC51E node::OnFatalError+270
 4: 00007FF6324EBA39 DSA_meth_get_flags+65833
 5: 00007FF63251DB8C napi_fatal_error+156
 6: 00007FFDBB392CC6
 7: 00007FFDBB377038
 8: 00007FFDBB38ED8C
 9: 00007FFDBB38E8EB
10: 00007FFDBB3ADE0D
11: 00007FFDBB3AEF1F
12: 00007FF63251B273 node_module_register+2403
13: 00007FF63251A995 node_module_register+133
14: 00007FF6325BEE4A uv_run+890
15: 00007FF632546384 v8::internal::CodeObjectRegistry::~CodeObjectRegistry+10996
16: 00007FF63255ED60 v8::internal::CodeObjectRegistry::~CodeObjectRegistry+111824
17: 00007FF63258B2D0 node::FreeEnvironment+112
18: 00007FF6324A41C5 cppgc::internal::Marker::visitor+57973
19: 00007FF63252332A node::Start+202
20: 00007FF6323486EC RC4_options+347628
21: 00007FF6333A92A8 v8::internal::compiler::RepresentationChanger::Uint32OverflowOperatorFor+14520
22: 00007FFDFE967614 BaseThreadInitThunk+20
23: 00007FFDFF5E26A1 RtlUserThreadStart+33

image

image

https://github.com/epics-base/epics-base/blob/7.0/modules/ca/src/client/oldChannelNotify.cpp

https://github.com/epics-base/epics-base/blob/7.0/modules/ca/src/client/cadef.h

Koromix commented 1 year ago

I'll look into this in a couple days ;)

wanglin86769 commented 1 year ago

Thanks for your interest in following up this problem. EPICS (Experimental Physics and Industrial Control System) is a powerful but complicated large-scale control system software toolkit.

I also reproduced this problem on Debian Linux 10.3 with the following steps:

  1. Download a version of EPICS base and unzip https://epics.anl.gov/download/base/base-3.15.9.tar.gz

  2. Install EPICS base to generate EPICS tools and libraries

    $ cd base-3.15.9/
    $ make
  3. Start an EPICS IOC (Input Output Controller)

    $ export PATH=$PATH:/home/debian/epics/base-3.15.9/bin/linux-x86_64
    $ softIoc -d dbExample1.db

    vi dbExample1.db

    record(ai, "aiExample")
    {
    field(DESC, "Analog input")
    #   field(INP, "calcExample.VAL  NPP NMS")
    field(EGUF, "10")
    field(EGU, "Counts")
    field(HOPR, "10")
    field(LOPR, "0")
    field(HIHI, "8")
    field(HIGH, "6")
    field(LOW, "4")
    field(LOLO, "2")
    field(HHSV, "MAJOR")
    field(HSV, "MINOR")
    field(LSV, "MINOR")
    field(LLSV, "MAJOR")
    field(VAL, "0.123456789")
    }
    record(calc, "calcExample")
    {
    field(DESC, "Counter")
    field(SCAN,"1 second")
    field(FLNK, "aiExample")
    field(CALC, "(A<B)?(A+C):D")
    field(INPA, "calcExample.VAL  NPP NMS")
    field(INPB, "70")
    field(INPC, "1")
    field(INPD, "30")
    field(EGU, "Counts")
    field(HOPR, "10")
    field(HIHI, "8")
    field(HIGH, "6")
    field(LOW, "4")
    field(LOLO, "2")
    field(HHSV, "MAJOR")
    field(HSV, "MINOR")
    field(LSV, "MINOR")
    field(LLSV, "MAJOR")
    }
    record(calc, "calcExample1")
    {
        field(DESC, "Counter")
        field(SCAN,"2 second")
        field(CALC, "(A<B)?(A+C):D")
        field(INPA, "calcExample1.VAL  NPP NMS")
        field(INPB, "80")
        field(INPC, "1")
        field(INPD, "20")
        field(EGU, "Counts")
        field(HOPR, "10")
        field(HIHI, "8")
        field(HIGH, "6")
        field(LOW, "4")
        field(LOLO, "2")
        field(HHSV, "MAJOR")
        field(HSV, "MINOR")
        field(LSV, "MINOR")
        field(LLSV, "MAJOR")
    }
    record(waveform, "wfExample")
    {
        field(DTYP, "Soft Channel")
        field(FTVL, "DOUBLE")
        field(NELM, "1024")
    }
  4. Test if the EPICS IOC is started successfully and PV (Process Variable) is accessible

image

image

  1. Execute node.js code with koffi to call shared libraries from EPICS base and connect to the EPICS PV
    $ export PATH=$PATH:/home/debian/epics/base-3.15.9/bin/linux-x86_64
    $ node ca.js

    vi ca.js

    
    const koffi = require('koffi');

const libca = koffi.load('/home/debian/epics/base-3.15.9/lib/linux-x86_64/libca.so');

let pointer = koffi.pointer('pointer', koffi.opaque(), 2); let chanId = koffi.pointer('chanId', koffi.opaque());

const evargs_t = koffi.struct('evargs_t', { usr: 'void ', chid: chanId, type: 'long', count: 'long', dbr: 'void ', status: 'int' }); const MonitorCallback = koffi.callback('MonitorCallback', 'void', [evargs_t]); let cb1 = koffi.register(args => { console.log(' cb1 '); }, koffi.pointer(MonitorCallback));

const evargs1_t = koffi.struct('evargs1_t', { chid: chanId, op: 'long' }); const ConnectionCallback = koffi.callback('ConnectionCallback', 'void', ['evargs1_t']); let cb2 = koffi.register(function(args) { console.log(' cb2 '); console.log(args.chid); console.log(args.op); }, 'ConnectionCallback *');

const ca_context_create = libca.func('ca_context_create', 'int', ['int']); const ca_pend_event = libca.func('ca_pend_event', 'int', ['double']); const ca_pend_io = libca.func('ca_pend_io', 'int', ['double']); const ca_create_channel = libca.func('ca_create_channel', 'int', ['string',koffi.pointer(ConnectionCallback),'pointer','int',koffi.out(pointer)]); const ca_field_type = libca.func('ca_field_type', 'short', [chanId]); const ca_element_count = libca.func('ca_element_count', 'int', [chanId]); const ca_array_get = libca.func('ca_array_get', 'int', ['int','ulong',chanId,koffi.out('void *')]); const ca_array_get_callback = libca.func('ca_array_get_callback', 'int', ['int','ulong',chanId,'pointer','pointer']); const ca_create_subscription = libca.func('ca_create_subscription', 'int', ['int','ulong',chanId,'long',koffi.pointer(MonitorCallback),'pointer','pointer']); const ca_clear_channel = libca.func('ca_clear_channel', 'int', [chanId]);

let chidPtr = [null]; let priority = 0; ca_context_create(1); ca_create_channel('calcExample', null, null, priority, chidPtr); ca_pend_io(1.0); let chid = chidPtr[0]; fieldType = ca_field_type(chidPtr[0]); count = ca_element_count(chidPtr[0]); console.log(fieldType); console.log(count); console.log(' Before subscription '); ca_create_subscription(fieldType, count, chid, 1, cb1, null, null); console.log(' After subscription '); ca_pend_io(1.0); ca_pend_event(1.0);



If the EPICS PV can be connected, the error information is as follows:

![image](https://user-images.githubusercontent.com/62229607/215817969-b46beeca-d633-4f78-9dac-51a475a81fef.png)
wanglin86769 commented 1 year ago

There are already two node.js implementations of EPICS CA (Channel Access):

  1. node-epics using ffi https://github.com/RobbieClarken/node-epics/blob/master/lib/ca.js

  2. epics-ioc-connection using ffi-napi https://github.com/onichandame/epics-ioc-connection/blob/master/src/ca/channel.ts

However, both these two implementations as well as the ffi libraries seem to be unmaintained.

Koromix commented 1 year ago

Thanks for the instructions, that'll make it much easier for me :) I'll look into this soon.

wanglin86769 commented 1 year ago

Similar to Issue https://github.com/Koromix/koffi/issues/39 After adding the following lines to prevent the JavaScript main thread from exiting too early, the callback can execute.

setTimeout(function() {
  console.log("Good Night!");
}, 5000);
wanglin86769 commented 1 year ago

I have one more question. Some arguments of the callback are Napi::External data type, how can I extract the actual data from it in JavaScript code? For example, the "dbr" field as follows,

{
  usr: null,
  chid: [External: 264d728e750],
  type: 6,
  count: 1,
  dbr: [External: 264d72a5a80],
  status: 1
}
Koromix commented 1 year ago

Indeed, sorry for not answering. Node.js is single-threaded, you cannot execute JS code in anything other than the main thread.

When a callback is called from a secondary thread, Koffi just asks the main thread to execute it, and blocks until it's done. Obviously if the main thread is busy this does not work and hangs. It needs to run the event loop, which is what happens when you can setTimeout, or if there is an Electron window running, or the main thread is awaiting for something, etc.

There is some info about this here: https://koffi.dev/callbacks#asynchronous-callbacks Also, note that warning at the end of the section.

Koromix commented 1 year ago

You can use koffi.decode() to convert the external value to a JS value (an object, an int, whatever).

Koffi does not do it automatically because it does not have enough information at this point: the pointer could be invalid, or it could point to many values, etc.

More info and an example here: https://koffi.dev/callbacks#decoding-pointer-arguments