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

Dynamic loading sample #645

Closed
wants to merge 7 commits into from
Closed

Dynamic loading sample #645

wants to merge 7 commits into from

Conversation

goto-bus-stop
Copy link
Member

@goto-bus-stop goto-bus-stop commented Nov 26, 2020

This PR implements the approach described in #584, and uses it in the functions in neon_runtime::napi::primitive.

The N-API symbols are loaded from the host process, which could be Node.js or Electron. We have to provide our own N-API type declarations with this approach. Since N-API is ABI-stable, that should not require a lot of maintenance.

Here, we use libloading to retrieve symbols from the host executable. We store raw function pointers, bypassing some libloading safety features. This is OK for us because we run as a module inside the host executable: the executable will not be unloaded before we are, so the pointers will not become invalid at any point while we are running. Currently, this sample uses hand-written trampoline functions to call the actual function pointers. The original PR used a macro (described below).

Original PR description

Some changes were made to the approach. This was the original description:

The gist of the system is:

struct Napi<'a> {
  napi_functionname: Symbol<'a, unsafe extern "C" fn(argtypes...) -> NapiStatus>,
}
impl Napi<'_> {
  pub fn from_host() -> Self {
    Self {
      napi_functionname: load_host_library().get("napi_functionname").unwrap(),
    }
  }
}

An instance of this Napi struct is initialized when the Neon module loads. Calls to N-API functions are made > like:

let status = napi!(napi_functionname(args...));

Which translates to something like

let status = ((*crate::bindings::NAPI).napi_functionname)(args...);
Silly(?) macro idea

Maybe we can make a macro like this to ease future maintenance:

declare_napi_functions! {
    fn napi_get_undefined(env: NapiEnv, out: *mut NapiValue) -> NapiStatus;
    fn napi_get_null(env: NapiEnv, out: *mut NapiValue) -> NapiStatus;

    fn napi_get_boolean(env: NapiEnv, value: bool, out: *mut NapiValue) -> NapiStatus;
    fn napi_get_value_bool(env: NapiEnv, value: NapiValue, out: *mut bool) -> NapiStatus;

    fn napi_create_double(env: NapiEnv, value: f64, out: *mut NapiValue) -> NapiStatus;
    fn napi_get_value_double(env: NapiEnv, value: NapiValue, out: *mut f64) -> NapiStatus;
}

I think it would have to be a proc macro, since we need to output those declarations several
times in different shapes. It would allow generating both the property declarations in the Napi
struct, and the library.get() calls in the from_host constructor. It could also generate
trampoline functions so we don't have to use an napi!() macro wrapper for napi function calls:

pub unsafe fn napi_get_undefined(env: NapiEnv, out: *mut NapiValue) -> NapiStatus {
    ((*NAPI).napi_get_undefined)(env, out)
}

Might well be a case where writing the abstraction would be a lot more work than just doing it by hand, though.

@@ -5,16 +5,20 @@ authors = ["Dave Herman <[email protected]>"]
description = "Bindings to the Node.js native addon API, used by the Neon implementation."
repository = "https://github.com/neon-bindings/neon"
license = "MIT/Apache-2.0"
edition = "2018"
Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to edition 2018 because I only know how to use macro_rules! in this edition

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
pub(crate) enum NapiStatus {
Copy link
Member Author

Choose a reason for hiding this comment

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

Copy-paste from the N-API docs; with repr(C) this is fine. N-API will only ever add new values at the end

}

pub(crate) struct Napi<'a> {
pub napi_get_undefined: Symbol<'a, unsafe extern "C" fn(env: NapiEnv, out: *mut NapiValue) -> NapiStatus>,
Copy link
Member Author

Choose a reason for hiding this comment

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

I think the actual declaration is quite hard to read here, because of the Symbol<> wrapper and because it's in the middle of the line instead of at the start. Could we make it simpler somehow?

@kjvalencik
Copy link
Member

@goto-bus-stop This is really awesome! I think you're right that it would need to be a proc-macro because of the function arguments; I don't think there's a reasonable way to match all the variations there. But, I think a proc-macro might make this a bit too complicated. We could potentially write some python or node.js to generate it offline.

Overall, I really like this strategy. I think it's worth writing the trampoline functions to eliminate the napi! macro and hopefully end up with a fast path. Here's a potential option:

fn must_initialize() -> ! {
    panic!("Must initialize N-API calls");
}

unsafe extern "C" fn _napi_get_undefined(
    _env: NapiEnv,
    _out: *mut NapiValue,
) -> NapiStatus {
    must_initialize()
}

static mut NAPI_GET_UNDEFINED: unsafe extern "C" fn(
    NapiEnv,
    *mut NapiValue,
) -> NapiStatus = _napi_get_undefined;

#[inline]
pub(crate) unsafe fn napi_get_undefined(
    env: NapiEnv,
    out: *mut NapiValue,
) -> NapiStatus {
    NAPI_GET_UNDEFINED(env, out)
}

unsafe extern "C" fn _napi_create_double(
    _env: NapiEnv,
    _value: f64,
    _out: *mut NapiValue,
) -> NapiStatus {
    must_initialize()
}

static mut NAPI_CREATE_DOUBLE: unsafe extern "C" fn(
    NapiEnv,
    f64,
    *mut NapiValue,
) -> NapiStatus = _napi_create_double;

#[inline]
pub(crate) unsafe fn napi_create_double(
    env: NapiEnv,
    value: f64,
    out: *mut NapiValue,
) -> NapiStatus {
    NAPI_CREATE_DOUBLE(env, value, out)
}

It's very verbose, but fairly straightforward. Each function has 3 copies of the signature:

  • Empty initialization prefixed with _ (e.g., _napi_create_double) that panics
  • Static fn pointer (e.g., NAPI_CREATE_DOUBLE) initialized with the panicking default
  • Trampoline fn that executes the static. This keeps the call sites simple. These are marked inline for performance. Running dynamically loaded n-api methods should be only a single extra pointer lookup.

Unfortunately, the never type (!) isn't stabilized so we might need a distinct panic in each. If this blows up the binary size, there's some super unsafe transmute magic that can be used to eliminate it.

With the trampoline functions, I don't think we need lazy_static. Instead, we can leak the Library.

@goto-bus-stop
Copy link
Member Author

(FWIW the approach in this PR also compiles to a single indirect call in release mode)

@goto-bus-stop
Copy link
Member Author

I agree that a proc macro would be too complicated, I guess I just wanted to explore that a bit after the recent #[neon::main] addition 😄

I do like the simplicity of that function pointer approach. Having to also duplicate the arguments list is a bit unfortunate. Maybe it's fine since the compiler will yell at you if they are out of sync, and it should be one of the least-modified parts of the codebase.

The lazy_static isn't strictly necessary for this PR either. I figured that the cost of lazy_static is irrelevant since it's only paid once during initialization. But there is also a cognitive cost to having the lifetimes on the Napi struct where really that could just be 'static…

@goto-bus-stop
Copy link
Member Author

goto-bus-stop commented Nov 27, 2020

Last commit removes the napi!() macro in favour of manually written trampoline functions. I don't hate it? It definitely is quite verbose…

@goto-bus-stop
Copy link
Member Author

6ce7cf0 removes the Symbol<> wrapping and use of lazy_static!. I could go either way on that, I guess

@kjvalencik kjvalencik mentioned this pull request Nov 30, 2020
@kjvalencik
Copy link
Member

I gave it a try. Lots and lots of typing. 😆

#646

@kjvalencik kjvalencik deleted the dynsym branch December 7, 2020 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants