Nim 开发 DLL 的注意事项
2024-11-24 13:21:36

此文基于 Nim 2.2.0 版本编写,不排除某些细节在未来的版本中发生变化。

调用约定

导出函数默认使用cdecl约定。

1
2
3
4
5
proc Add(arg1: cint, arg2: cint): cint {.exportc, dynlib} =
return arg1 + arg2

# 生成的 C 代码
# N_LIB_EXPORT N_CDECL(int, Add)(int arg1_p0, int arg2_p1);

NimMain、NimDestroyGlobals 函数

Nim 编译的 DLL 文件会强制导出NimMainNimDestroyGlobals函数。


NimMain的作用是什么?看看生成的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
N_LIB_PRIVATE N_CDECL(void, NimMainInner)(void) {
NimMainModule();
}

N_LIB_EXPORT N_CDECL(void, NimMain)(void) {
#if 0
void (*volatile inner)(void);
PreMain();
inner = NimMainInner;
(*inner)();
#else
PreMain();
NimMainInner();
#endif
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fwdreason,
LPVOID lpvReserved) {
if (fwdreason == DLL_PROCESS_ATTACH) {
NimMain();
}
return 1;
}

N_LIB_PRIVATE N_NIMCALL(void, NimMainModule)(void) {
{
tyObject_MyTypecolonObjectType___hPGAy5oGaUa5w9ctac4yDNg* T1_;
nimfr_("mylib", "C:\\apps\\mylib.nim");
nimln_(9); T1_ = NIM_NIL;
T1_ = (tyObject_MyTypecolonObjectType___hPGAy5oGaUa5w9ctac4yDNg*) nimNewObjUninit(sizeof(tyObject_MyTypecolonObjectType___hPGAy5oGaUa5w9ctac4yDNg), NIM_ALIGNOF(tyObject_MyTypecolonObjectType___hPGAy5oGaUa5w9ctac4yDNg));
(*T1_).field = ((NI)88);
globalVarRefObj__mylib_u7 = T1_;
nimln_(13); echoBinSafe(TM__wfpdBMSRfuTGK9aKTtj9beBg_2, 1);
nimTestErrorFlag();
popFrame();
}}

挺明显的,DLL 被加载时会依次调用:

1
DllMain > NimMain > NimMainInner > NimMainModule

可以看出NimMain用于初始化动态库资源。
其中NimMainModule是顶级语句执行的地方,这里会初始化全局变量,执行顶级语句。
可以在编译时用--nimMainPrefix选项为NimMain函数添加前缀名称,比如--nimMainPrefix:My会将导出名称变为MyNimMain


--noMain编译选项用于禁止生成DllMain方法。使用此选项后就需要手动调用NimMain来初始化动态库。
根据 Windows 上 DLL 开发经验来看,DllMain 存在死锁的风险。除非初始化足够简单,否则推荐手动调用NimMain方法,此时就需要用到--noMain编译选项。


NimDestroyGlobals则用于在卸载 DLL 前释放资源。Nim 不会主动去调用它,需要你手动调用。

1
2
3
N_LIB_EXPORT N_CDECL(void, NimDestroyGlobals)(void) {
eqdestroy___mylib_u17(globalVarRefObj__mylib_u7);
}

NimDestroyGlobals也是同理。希望以后的版本中能不要强制导出该函数,而是给开发者自行在内部调用的机会。

内部手动调用 NimMain

通常开发 DLL 时都会导出自己的 Init、Uninit 方法,我们可能不希望用户去调用 Nim 开头的方法,但我们又需要在内部调用它来初始化 GC 实例。
可以通过非常规手段,因为 Nim 最终生成的是 C,所以可以在 Nim 中插入 C 代码来调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
{.emit: """
# 提前声明
N_LIB_EXPORT N_CDECL(void, NimMain)(void);

# 中转函数
static void CallNimMain() { NimMain(); }
""".}

proc CallNimMain() {.importc, cdecl.}

proc MyInit() =
CallNimMain()
echo "Hello from our dynamic library!"

useNimRtl

Nim 是有 GC 的语言,默认情况下每个模块都有自己的 GC 实例。但如果项目中包含多个 Nim 开发的模块,且互相之间会传递引用数据类型或异常,那么必须为所有模块指定一个公共的 GC 实例。
这可以通过nimrtl.dll来实现,进入 Nim 安装目录,编译nimrtl.nim

1
nim c -d:release lib/nimrtl.nim

接着在所有模块编译时加上-d:useNimRtl选项,这告诉 Nim 生成的 C 代码中关于资源管理的方法都调用nimrtl.dll中的方法,而不是自己实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 未使用 -d:useNimRtl 时
N_LIB_PRIVATE N_NOCONV(void, deallocShared)(void* p_p0);
static N_INLINE(NIM_BOOL*, nimErrorFlag)(void);
N_LIB_PRIVATE N_NIMCALL(void, nimTestErrorFlag)(void);
static N_INLINE(void, nimFrame)(TFrame* s_p0);
static N_INLINE(void, popFrame)(void);

// 使用 -d:useNimRtl 后,相关方法都是从外部引用的
extern tyProc__y9ajMVPY4VS41rTmWR9avTRA deallocShared;
extern tyProc__Gs82igvKkcgSIofZEvpV9cw nimErrorFlag;
extern tyProc__ln4kdL5W9bbX4a1xl8nnVXQ nimTestErrorFlag;
extern tyProc__ijRNnOwJxOTZNE1aedFnKQ nimFrame;
extern tyProc__ln4kdL5W9bbX4a1xl8nnVXQ popFrame;

当然,如果模块之间使用的是 C ABI,那么就不需要引入nimrtl

总结

  • 动态库强制导出NimMainNimDestroyGlobals函数,用于初始化 GC 和释放资源。
  • --noMain编译选项禁止生成DllMain方法,这样就避免自动执行NimMain函数。
  • --nimMainPrefix编译选项为NimMain函数名称添加前缀。
  • 当项目中包含多个 Nim 模块时且存在类型依赖时,建议使用-d:useNimRtl编译选项来使用nimrtl.dll中的 GC 实例。

相关阅读

Dynamic libraries in Nim
DLL generation