io.jsでネイティブ・アドオンが動かない原因を調べる - エラーが何を示しているのかを調べる。

公開:2015-04-14 21:52
更新:2020-02-15 04:37
カテゴリ:io.js,native addon,c++,javascript

このエラーについて調べてみた。

H:\pj\gyptest>node.exe np.js
module.js:335
  Module._extensionsextension;
                               ^
Error: Module did not self-register.
    at Error (native)
    at Module.load (module.js:335:32)
    at Function.Module._load (module.js:290:12)
    at Module.require (module.js:345:17)
    at require (module.js:364:17)
    at Object.<anonymous> (H:\pj\gyptest\np.js:1:77)
    at Module._compile (module.js:410:26)
    at Object.Module._extensions..js (module.js:428:10)
    at Module.load (module.js:335:32)
    at Function.Module._load (module.js:290:12)
    

エラーによれば、module.jsの335行目でエラーが発生している。

// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
debug('load ' + JSON.stringify(filename) +
' for module ' + JSON.stringify(this.id));
assert(!this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);// <<<<< エラー発生個所
this.loaded = true;
};
view raw gistfile1.js hosted with ❤ by GitHub

拡張子によってthis,filenameを引数に取る関数を呼び分けるようになっている。node拡張子の時はprocess.dlopen()を呼び出すようになっていた。

//Native extension for .node
Module._extensions['.node'] = process.dlopen;
view raw gistfile1.js hosted with ❤ by GitHub

続いてdlopen関数を調べる。dlopen関数はnode.ccファイルの中にDLOpen関数として定義されていた。つまりネイティブ・メソッドである。ここからはC/C++の世界である。

// DLOpen is process.dlopen(module, filename).
// Used to load 'module.node' dynamically shared objects.
//
// FIXME(bnoordhuis) Not multi-context ready. TBD how to resolve the conflict
// when two contexts try to load the same shared object. Maybe have a shadow
// cache that's a plain C list or hash table that's shared across contexts?
void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;
CHECK_EQ(modpending, nullptr);
if (args.Length() != 2) {
env->ThrowError("process.dlopen takes exactly 2 arguments.");
return;
}
Local<Object> module = args[0]->ToObject(env->isolate()); // Cast
node::Utf8Value filename(env->isolate(), args[1]); // Cast
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
// Objects containing v14 or later modules will have registered themselves
// on the pending list. Activate all of them now. At present, only one
// module per object is supported.
node_module* const mp = modpending;
modpending = nullptr;
if (is_dlopen_error) {
Local<String> errmsg = OneByteString(env->isolate(), uv_dlerror(&lib));
#ifdef _WIN32
// Windows needs to add the filename into the error message
errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate()));
#endif // _WIN32
env->isolate()->ThrowException(Exception::Error(errmsg));
return;
}
if (mp == nullptr) {
env->ThrowError("Module did not self-register.");
return;
}
if (mp->nm_version != NODE_MODULE_VERSION) {
char errmsg[1024];
snprintf(errmsg,
sizeof(errmsg),
"Module version mismatch. Expected %d, got %d.",
NODE_MODULE_VERSION, mp->nm_version);
env->ThrowError(errmsg);
return;
}
if (mp->nm_flags & NM_F_BUILTIN) {
env->ThrowError("Built-in module self-registered.");
return;
}
mp->nm_dso_handle = lib.handle;
mp->nm_link = modlist_addon;
modlist_addon = mp;
Local<String> exports_string = env->exports_string();
Local<Object> exports = module->Get(exports_string)->ToObject(env->isolate());
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
} else {
env->ThrowError("Module has no declared entry point.");
return;
}
// Tell coverity that 'handle' should not be freed when we return.
// coverity[leaked_storage]
}
view raw gistfile1.cpp hosted with ❤ by GitHub

38-41行目でmpがnullptrの時に「Module did not self-register.」エラーが発生するようである。このmpはnode_module構造体へのポインタである。25行目でmpにmodpendingの内容を代入している。このmodpendingというのはstaticグローバル変数である。

static node_module* modpending;
view raw gistfile1.cpp hosted with ❤ by GitHub

先ほどのDLOpen関数に戻ると11行目でmodpendingがnullptrかどうかをテストしている。それから25行目でmpにmodpendingを代入しているから、その間にmodpendingに値がセットされるはずである。怪しいのは20行目のuv_dlopen関数である。これを調べる。

このuv_dlopen関数は以下の通りであった。私はWindowsしか知らないのでwin版のdl.c中のを参照した。

int uv_dlopen(const char* filename, uv_lib_t* lib) {
WCHAR filename_w[32768];
lib->handle = NULL;
lib->errmsg = NULL;
if (!uv_utf8_to_utf16(filename, filename_w, ARRAY_SIZE(filename_w))) {
return uv__dlerror(lib, GetLastError());
}
lib->handle = LoadLibraryExW(filename_w, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (lib->handle == NULL) {
return uv__dlerror(lib, GetLastError());
}
return 0;
}
view raw gistfile1.c hosted with ❤ by GitHub

ソースを読むとこの関数はdllを読み出しているだけである。ん、どこでmodpendingに値をセットしているのか。よくわからないのでio.jsのソースコードをmodpendingでgrepするとnode_module_register関数をnode.cc中で見つけた。

extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_BUILTIN) {
mp->nm_link = modlist_builtin;
modlist_builtin = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
modpending = mp;
}
}
view raw gistfile1.cpp hosted with ❤ by GitHub

この関数の15行目でmodpendingに値をセットしている。とするとこの関数がどこかで呼ばれ、modpendingに値をセットするのだということがわかる。で、今度はnode_module_registerでソースコードをgrepする。すると以下のようなマクロがnode.h中で見つかる。

#define NODE_MODULE_X(modname, regfunc, priv, flags) \
extern "C" { \
static node::node_module _module = \
{ \
NODE_MODULE_VERSION, \
flags, \
NULL, \
__FILE__, \
(node::addon_register_func) (regfunc), \
NULL, \
NODE_STRINGIFY(modname), \
priv, \
NULL \
}; \
NODE_C_CTOR(_register_ ## modname) { \
node_module_register(&_module); \
} \
}
view raw gistfile1.cpp hosted with ❤ by GitHub

このマクロ中の16行目、NODE_C_CTORマクロ中でnode_module_register関数は呼ばれている。ちなみにこのNODE_MODULE_Xマクロは何かというと、ネイティブ・アドオンモジュールを定義するときに使うマクロNODE_MODULEのベースとなるマクロである。NODE_MODULE_Xの引数を省略したものがNODE_MODULEである。

#define NODE_MODULE(modname, regfunc) \
NODE_MODULE_X(modname, regfunc, NULL, 0)
view raw gistfile1.cpp hosted with ❤ by GitHub

カギとなりそうなのはNODE_C_CTORマクロである。このマクロ定義をのぞいてみる。

#if defined(_MSC_VER)
#pragma section(".CRT$XCU", read)
#define NODE_C_CTOR(fn) \
static void __cdecl fn(void); \
__declspec(dllexport, allocate(".CRT$XCU")) \
void (__cdecl*fn ## _)(void) = fn; \
static void __cdecl fn(void)
#else
#define NODE_C_CTOR(fn) \
static void fn(void) __attribute__((constructor)); \
static void fn(void)
#endif
view raw gistfile1.cpp hosted with ❤ by GitHub

#if defined(_MSC_VER)で囲まれている部分を見るとこれは巧妙なハックであることがわかる。これはmain関数が呼ばれる前に初期化関数を呼び出す手法である。つまりはこのマクロはユーザー初期化セクション(.CRT$XCU)に初期化関数を定義しつつセットするのである。そうすることでモジュールがロードされたときにこの関数が呼び出されるのではないかと思う。この部分はDLLがロードされたときの動きがどうなるのかの正確な情報を得ていないので推測ではあるが。

まとめると、uv_dlopen()関数を呼び出したときにdllがロードされ初期化関数が実行されることでnode_module_register関数が呼ばれmodpendingに値がセットされるのである。

つまり「Module did not self-register.」エラーはmpがnullptrであった場合、つまりmodpendingに何らかの原因で値がセットされなかったときに発生するようだ。これが発生するケースとしてはこの初期化関数がdllロード時に呼ばれなかった場合が考えられるけれども、なぜnode.exeのときにそうなってしまうのかはよくわからない。というかそうなるという裏付けも取れていないのだけれども。もう少し調べてみようかなと思っている。