From 5c5bb7626c825ec411fe4fd3936464991520d508 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Tue, 2 Feb 2021 23:07:34 -0800 Subject: [PATCH] node-api: allow retrieval of add-on file name Unlike JS-only modules, native add-ons are always associated with a dynamic shared object from which they are loaded. Being able to retrieve its absolute path is important to native-only add-ons, i.e. add-ons that are not themselves being loaded from a JS-only module located in the same package as the native add-on itself. Currently, the file name is obtained at environment construction time from the JS `module.filename`. Nevertheless, the presence of `module` is not required, because the file name could also be passed in via a private property added onto `exports` from the `process.dlopen` binding. As an attempt at future-proofing, the file name is provided as a URL, i.e. prefixed with the `file://` protocol. Fixes: https://github.com/nodejs/node-addon-api/issues/449 PR-URL: https://github.com/nodejs/node/pull/37195 Backport-PR-URL: https://github.com/nodejs/node/pull/37327 Co-authored-by: Michael Dawson Reviewed-By: Michael Dawson --- doc/api/n-api.md | 25 +++++++++++++ src/env.h | 1 + src/node_api.cc | 45 ++++++++++++++++++++--- src/node_api.h | 7 ++++ test/node-api/test_general/test.js | 7 +++- test/node-api/test_general/test_general.c | 13 +++++++ 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index f57d40ba549a55..d25fc4193590d1 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -5938,6 +5938,31 @@ idempotent. This API may only be called from the main thread. +## Miscellaneous utilities + +## node_api_get_module_file_name + + + +> Stability: 1 - Experimental + +```c +NAPI_EXTERN napi_status +node_api_get_module_file_name(napi_env env, const char** result); + +``` + +* `[in] env`: The environment that the API is invoked under. +* `[out] result`: A URL containing the absolute path of the + location from which the add-on was loaded. For a file on the local + file system it will start with `file://`. The string is null-terminated and + owned by `env` and must thus not be modified or freed. + +`result` may be an empty string if the add-on loading process fails to establish +the add-on's file name during loading. + [ABI Stability]: https://nodejs.org/en/docs/guides/abi-stability/ [AppVeyor]: https://www.appveyor.com [C++ Addons]: addons.md diff --git a/src/env.h b/src/env.h index 6b66acf289fef0..d4d9c29118a9cb 100644 --- a/src/env.h +++ b/src/env.h @@ -249,6 +249,7 @@ constexpr size_t kFsStatsBufferLength = V(fd_string, "fd") \ V(fields_string, "fields") \ V(file_string, "file") \ + V(filename_string, "filename") \ V(fingerprint256_string, "fingerprint256") \ V(fingerprint_string, "fingerprint") \ V(flags_string, "flags") \ diff --git a/src/node_api.cc b/src/node_api.cc index de5727d0694ae9..3fcc9ecbf4a63f 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -16,8 +16,9 @@ #include struct node_napi_env__ : public napi_env__ { - explicit node_napi_env__(v8::Local context): - napi_env__(context) { + explicit node_napi_env__(v8::Local context, + const std::string& module_filename): + napi_env__(context), filename(module_filename) { CHECK_NOT_NULL(node_env()); } @@ -52,6 +53,10 @@ struct node_napi_env__ : public napi_env__ { }); }); } + + const char* GetFilename() const { return filename.c_str(); } + + std::string filename; }; typedef node_napi_env__* node_napi_env; @@ -93,10 +98,11 @@ class BufferFinalizer : private Finalizer { }; }; -static inline napi_env NewEnv(v8::Local context) { +static inline napi_env +NewEnv(v8::Local context, const std::string& module_filename) { node_napi_env result; - result = new node_napi_env__(context); + result = new node_napi_env__(context, module_filename); // TODO(addaleax): There was previously code that tried to delete the // napi_env when its v8::Context was garbage collected; // However, as long as N-API addons using this napi_env are in place, @@ -579,16 +585,35 @@ void napi_module_register_by_symbol(v8::Local exports, v8::Local module, v8::Local context, napi_addon_register_func init) { + node::Environment* node_env = node::Environment::GetCurrent(context); + std::string module_filename = ""; if (init == nullptr) { - node::Environment* node_env = node::Environment::GetCurrent(context); CHECK_NOT_NULL(node_env); node_env->ThrowError( "Module has no declared entry point."); return; } + // We set `env->filename` from `module.filename` here, but we could just as + // easily add a private property to `exports` in `process.dlopen`, which + // receives the file name from JS, and retrieve *that* here. Thus, we are not + // endorsing commonjs here by making use of `module.filename`. + v8::Local filename_js; + v8::Local modobj; + if (module->ToObject(context).ToLocal(&modobj) && + modobj->Get(context, node_env->filename_string()).ToLocal(&filename_js) && + filename_js->IsString()) { + node::Utf8Value filename(node_env->isolate(), filename_js); // Cast + + // Turn the absolute path into a URL. Currently the absolute path is always + // a file system path. + // TODO(gabrielschulhof): Pass the `filename` through unchanged if/when we + // receive it as a URL already. + module_filename = std::string("file://") + (*filename); + } + // Create a new napi_env for this specific module. - napi_env env = v8impl::NewEnv(context); + napi_env env = v8impl::NewEnv(context, module_filename); napi_value _exports; env->CallIntoModule([&](napi_env env) { @@ -1287,3 +1312,11 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func) { CHECK_NOT_NULL(func); return reinterpret_cast(func)->Ref(); } + +napi_status node_api_get_module_file_name(napi_env env, const char** result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + + *result = static_cast(env)->GetFilename(); + return napi_clear_last_error(env); +} diff --git a/src/node_api.h b/src/node_api.h index 7c2c84398cc5fe..1772c67c15afb2 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -263,6 +263,13 @@ NAPI_EXTERN napi_status napi_remove_async_cleanup_hook( #endif // NAPI_VERSION >= 8 +#ifdef NAPI_EXPERIMENTAL + +NAPI_EXTERN napi_status +node_api_get_module_file_name(napi_env env, const char** result); + +#endif // NAPI_EXPERIMENTAL + EXTERN_C_END #endif // SRC_NODE_API_H_ diff --git a/test/node-api/test_general/test.js b/test/node-api/test_general/test.js index 108d51a1e11ac9..dd409f010a3ada 100644 --- a/test/node-api/test_general/test.js +++ b/test/node-api/test_general/test.js @@ -1,9 +1,14 @@ 'use strict'; const common = require('../../common'); -const test_general = require(`./build/${common.buildType}/test_general`); +const filename = require.resolve(`./build/${common.buildType}/test_general`); +const test_general = require(filename); const assert = require('assert'); +// TODO(gabrielschulhof): This test may need updating if/when the filename +// becomes a full-fledged URL. +assert.strictEqual(test_general.filename, `file://${filename}`); + const [ major, minor, patch, release ] = test_general.testGetNodeVersion(); assert.strictEqual(process.version.split('-')[0], `v${major}.${minor}.${patch}`); diff --git a/test/node-api/test_general/test_general.c b/test/node-api/test_general/test_general.c index be805f782be8d5..5c2c2817312de1 100644 --- a/test/node-api/test_general/test_general.c +++ b/test/node-api/test_general/test_general.c @@ -1,3 +1,4 @@ +#define NAPI_EXPERIMENTAL #include #include #include "../../js-native-api/common.h" @@ -21,9 +22,21 @@ static napi_value testGetNodeVersion(napi_env env, napi_callback_info info) { return result; } +static napi_value GetFilename(napi_env env, napi_callback_info info) { + const char* filename; + napi_value result; + + NAPI_CALL(env, node_api_get_module_file_name(env, &filename)); + NAPI_CALL(env, + napi_create_string_utf8(env, filename, NAPI_AUTO_LENGTH, &result)); + + return result; +} + static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor descriptors[] = { DECLARE_NAPI_PROPERTY("testGetNodeVersion", testGetNodeVersion), + DECLARE_NAPI_GETTER("filename", GetFilename), }; NAPI_CALL(env, napi_define_properties(