used pragma
ふと、Nimで __attribute__((constructor))
相当を使いたいと思い、以下のようなコードを書いた。
var initCount: int
proc initializer() {.used, codegenDecl: "NIM_POSIX_INIT $# $#$#".} =
initCount += 1
doAssert initCount == 1
まず、Nimには __attribute__((constructor))
相当の機能を実現するようなpragmaはない。そこでどうするかというと、codegenDecl pragmaを用いてCで同等のコードを生成するような仕掛けを行った。
codegenDecl pragma
はバックエンドのCコンパイラがサポートしているものであれば任意の関数属性が付与できるため、この方法で__attribute__((destructor))
や__attribute__((cold))
なども付与することができる。
NIM_POSIX_INIT
はnimbase.hで定義されており、__attribute__((constructor))
を指すマクロ定義となっている。どこで使われているのか調べてみると、これは共有リンクライブラリを作成するときにNimのメイン関数を呼び出すために使っているようだ。
さて、これだけではNimレベルでみた場合コード上でどこからも参照されないprocになってしまう。
そこで最適化で消去されることを防ぐため、used pragmaも付与することを考えた。used pragma
のマニュアルでは未使用かつエクスポートされていないシンボルに対する警告の抑制として書かれているが、NimのコンパイラにはDead Code Eliminationによる最適化機能が含まれているため、消されては困る定義に対しては明示的にused pragma
を付与したほうがよさそうだ。
しかしこれは期待していた通りに動かない。
$ nim c -r --verbosity:0 pragma/crt_constructor.nim
(...)
Error: unhandled exception: /home/kubo39/dev/nim/d-to-nim/pragma/crt_constructor.nim(12, 1) `initCount == 1` [AssertionDefect]
Error: execution of an external program failed: '/home/kubo39/dev/nim/d-to-nim/pragma/crt_constructor'
結論からいうと、used pragmaへの理解が足りていなかったようだ。used pragmaはNimレベルでのコード上での未使用を検知はしてくれるが、バックエンドのコード生成でDead Code Eliminationが行われないことを保証するような機能ではない。__attribute__((used))
と同様の機能だが、生成するもののスコープ対象は異なっているわけだ。さらにここで__attribute__((used))
を付与したとしてもNimのDead Code EliminationはC言語の関数属性を考慮するわけではないので意味がない。
exportc pragma
それではどうすればいいかというと、exportc pragma
を使うことでbackend codegenにおけるDead Code Eliminationを回避することができる。
exportc pragmaは言語マニュアルによると、Nimの型・変数・ProcをC言語の対応する機能としてエクスポートするための機能となっている。このときexportcはその機能を実現するために、バックエンドのコード生成のDead Code Eliminationの対象から除外されるようになっている。
proc isExportedToC(c: var AliveContext; g: PackedModuleGraph; symId: int32): bool =
## "Exported to C" procs are special (these are marked with '.exportc') because these
## must not be optimized away!
let symPtr = unsafeAddr g[c.thisModule].fromDisk.syms[symId]
let flags = symPtr.flags
# due to a bug/limitation in the lambda lifting, unused inner procs
# are not transformed correctly; issue (#411). However, the whole purpose here
# is to eliminate unused procs. So there is no special logic required for this case.
if sfCompileTime notin flags:
if ({sfExportc, sfCompilerProc} * flags != {}) or
(symPtr.kind == skMethod):
result = true
# XXX: This used to be a condition to:
# (sfExportc in prc.flags and lfExportLib in prc.loc.flags) or
if sfCompilerProc in flags:
c.compilerProcs[g[c.thisModule].fromDisk.strings[symPtr.name]] = (c.thisModule, symId)
もちろんNimレベルでも最適化で消えてしまっては困るので、used pragma相当のフラグも暗黙に付与されている。
of wExportc, wExportCpp:
makeExternExport(c, sym, getOptionalStr(c, it, "$1"), it.info)
if k == wExportCpp:
if c.config.backend != backendCpp:
localError(c.config, it.info, "exportcpp requires `cpp` backend, got: " & $c.config.backend)
else:
incl(sym.flags, sfMangleCpp)
incl(sym.flags, sfUsed) # avoid wrong hints
まとめ
- Nimレベルでどこからも参照されないがC言語のコード生成対象に含んでほしいprocを定義する場合はexportc pragmaを使ってDead Code Eliminationの対象から除外させよう。
- exportc pragmaはused pragmaも内包するので、わざわざused pragmaも付与する必要はない。実はextern pragmaも同様に内包されている。
おまけ
実は__attribute__((used))
を付与しただけでは最終的なコード生成に残らない場合がある。Linkerに--gc-sections
が渡されてしまうことがあるからだ。そのため、GCCには__attribute__((retain))
という関数属性があり、これによってLinkerによるGCの削除対象から外すことができる。__attribute__((retain))
は逆にコンパイラに対しては最適化の抑止になならないので、必ず消されてほしくない場合は両方を付与するべき(__attribute__((used, retain))
)である。
では__attribute__((constructor))
を使う場合にこれらが必要ではないのか?という疑問が出てくる。しかし、そもそも__attribute__((constructor))
はlibcのスタートアップルーチン(.init_array
/.ctor
セクションがあるか)やLinker script(KEEP(*(.init_array .init_array.*))
のような記述がされているか)に依存しているため、追加でこれらの関数属性を付け足す意味はないだろう。