此文基于 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 文件会强制导出NimMain
和NimDestroyGlobals
函数。
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
| 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);
extern tyProc__y9ajMVPY4VS41rTmWR9avTRA deallocShared; extern tyProc__Gs82igvKkcgSIofZEvpV9cw nimErrorFlag; extern tyProc__ln4kdL5W9bbX4a1xl8nnVXQ nimTestErrorFlag; extern tyProc__ijRNnOwJxOTZNE1aedFnKQ nimFrame; extern tyProc__ln4kdL5W9bbX4a1xl8nnVXQ popFrame;
|
当然,如果模块之间使用的是 C ABI,那么就不需要引入nimrtl
。
总结
- 动态库强制导出
NimMain
和NimDestroyGlobals
函数,用于初始化 GC 和释放资源。
--noMain
编译选项禁止生成DllMain
方法,这样就避免自动执行NimMain
函数。
--nimMainPrefix
编译选项为NimMain
函数名称添加前缀。
- 当项目中包含多个 Nim 模块时且存在类型依赖时,建议使用
-d:useNimRtl
编译选项来使用nimrtl.dll
中的 GC 实例。
相关阅读
Dynamic libraries in Nim
DLL generation