Nimのused pragmaとexportc pragmaについて

## used pragma

ふと、Nimで `__attribute__((constructor))` 相当を使いたいと思い、以下のようなコードを書いた。

```nim
var initCount: int

proc initializer() {.used, codegenDecl: "NIM_POSIX_INIT $# $#$#".} =
  initCount += 1

doAssert initCount == 1
```

まず、Nimには `__attribute__((constructor))` 相当の機能を実現するようなpragmaはない。そこでどうするかというと、[codegenDecl pragma](https://nim-lang.github.io/Nim/manual.html#implementation-specific-pragmas-codegendecl-pragma)を用いてCで同等のコードを生成するような仕掛けを行った。
`codegenDecl pragma`はバックエンドのCコンパイラがサポートしているものであれば任意の関数属性が付与できるため、この方法で`__attribute__((destructor))`や`__attribute__((cold))`なども付与することができる。

`NIM_POSIX_INIT`はnimbase.hで定義されており、`__attribute__((constructor))`を指すマクロ定義となっている。どこで使われているのか調べてみると、これは共有リンクライブラリを作成するときにNimのメイン関数を呼び出すために使っているようだ。

さて、これだけではNimレベルでみた場合コード上でどこからも参照されないprocになってしまう。
そこで最適化で消去されることを防ぐため、[used pragma](https://nim-lang.github.io/Nim/manual.html#implementation-specific-pragmas-codegendecl-pragma)も付与することを考えた。`used pragma`のマニュアルでは未使用かつエクスポートされていないシンボルに対する警告の抑制として書かれているが、NimのコンパイラにはDead Code Eliminationによる最適化機能が含まれているため、消されては困る定義に対しては明示的に`used pragma`を付与したほうがよさそうだ。

しかしこれは期待していた通りに動かない。

```console
$ 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は[言語マニュアル](https://nim-lang.github.io/Nim/manual.html#foreign-function-interface-exportc-pragma)によると、Nimの型・変数・ProcをC言語の対応する機能としてエクスポートするための機能となっている。このときexportcはその機能を実現するために、バックエンドのコード生成のDead Code Eliminationの対象から除外されるようになっている。

- https://github.com/nim-lang/Nim/blob/7f6e800cafc7b73625893fb5280eb8b51a15b252/compiler/ic/dce.nim#L31-L46

```nim
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相当のフラグも暗黙に付与されている。

- https://github.com/nim-lang/Nim/blob/fd4e3ae3e4564525f901f2517711a1243535d2a2/compiler/pragmas.nim#L846-L853

```nim
      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.*))`のような記述がされているか)に依存しているため、追加でこれらの関数属性を付け足す意味はないだろう。