Windows版Emacsを28.1に上げたのでNative Compilationフィーバーに便乗する

# はじめに

いやフィーバー(ふっるぅ)してないのかもしれませんが、先駆者の方がいろいろ情報を提供してくれているので、それに便乗してみたいと思います。私のオリジナルは、ほんの少しです。だからWindowsでEmacs Native Compilationオルタみたいな感じでお願いします。

# WindowsのGCC実行環境
EmacsのNative Compilationを使う場合、GCCの実行環境が必要となります。.el->.elc->.elnという順にコンパイルされていき、.elnがネイティブな実行コードです。ここではWindowsに限定しているので、.elnはx86のバイナリということになります。まぁ。最近はarmという選択肢もありますが。

.elcから.elnへのコンパイルにはGCCのJITコンパイルが使用されます。このためGCCでコンパイルができる環境が無いとNative Compilationは使用できません。LinuxやBSDなどはgcc関連のパッケージをインストールするだけで良くて、Emacsなんか使っているような方はインストールしている人が多いのではないかと思います。これに対してWindowsでは公式から配布されているバイナリが<a href="https://www.msys2.org">MSYS2</a>を使ってコンパイルされているのでMSYS2のインストールが必要となります。

もしMSYS2のインストールに躊躇が無いということであれば、MSYS2をインストールしシステムワイドにパスを通せばNative Compilationが使用できると思います。この方法を選択される場合は、ここで終了です。以降の内容は読んでもあまり参考にならないと思います。

# MSYS2をインストールしないで使う。
MSYS2自体、そこそこ大きいシステムですし、cygwinのような環境を使っていると、そっちと同じコマンド名などあって干渉してしまいます。また公式配布物のdllはMSYS2のdll(正確にはmingw64or32だと思いますが)で、それをEmacsローカルで使用するようになっています。
Native Compilationも同じような感じで使いたいというのが、このドキュメントの主な内容です。

# Native Compilationで必要なexeとdll
大前提として必要なexeとdllを入手するために、MSYS2の環境が必要です。ですので一度はインストールする必要があります。MSYS2の環境は特定のディレクトリ配下にすべてインストールされるので、アンインストールで環境が汚れるとかいうことはありません(レジストリは多少汚れると思いますが)。別は方法としてHyper-Vで仮想環境を作り、そちらにインストールして必要ファイルの入手後、削除するという方法もあると思います。この場合はライセンスに注意してください。無料の試用期間でそういうことやって良いのか私もわかりません。

必要なファイルは先駆者の方々がまとめてくれていますが、以下に載せておきます。

| ファイル名                       |
|:---------------------------------|
| MSYS2インストールDIR/mingw64/lib/libadvapi32.a |
| MSYS2インストールDIR/mingw64/lib/libgcc_s.a |
| MSYS2インストールDIR/mingw64/lib/libkernel32.a  |
| MSYS2インストールDIR/mingw64/lib/libmingw32.a |
| MSYS2インストールDIR/mingw64/lib/libmingwex.a |
| MSYS2インストールDIR/mingw64/lib/libmoldname.a |
| MSYS2インストールDIR/mingw64/lib/libmsvcrt.a |
| MSYS2インストールDIR/mingw64/lib/libpthread.a |
| MSYS2インストールDIR/mingw64/lib/libshell32.a |
| MSYS2インストールDIR/mingw64/lib/libuser32.a |
| MSYS2インストールDIR/mingw64/lib/gcc/x86_64-w64-mingw32/11.3.0/libgcc.a |
| MSYS2インストールDIR/mingw64/lib/crtbegin.o |
| MSYS2インストールDIR/mingw64/lib/crtend.o |
| MSYS2インストールDIR/mingw64/lib/dllcrt2.o |
| MSYS2インストールDIR/mingw64/bin/ld.exe |
| MSYS2インストールDIR/mingw64/bin/as.exe |
| MSYS2インストールDIR/mingw64/bin/libgccjit-0.dll |

# MSYS2をインストールしていない環境でのsearch dir
GCCの実行環境では、dll以外の必要なファイルはsearch dirという仕組みで検索されます。どのディレクトリがsearch dirとなっているかは、以下のコマンドで確認できます。

```Bash
$ gcc -print-search-dirs
install: C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/
programs: =C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/;C:/msys64/mingw64/bin/../lib/gcc/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../x86_64-w64-mingw32/bin/x86_64-w64-mingw32/11.3.0/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../x86_64-w64-mingw32/bin/
libraries: =C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/;C:/msys64/mingw64/bin/../lib/gcc/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../x86_64-w64-mingw32/lib/x86_64-w64-mingw32/11.3.0/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../x86_64-w64-mingw32/lib/../lib/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../x86_64-w64-mingw32/11.3.0/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../lib/;D:/a/msys64/mingw64/lib/x86_64-w64-mingw32/11.3.0/;D:/a/msys64/mingw64/lib/../lib/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../../x86_64-w64-mingw32/lib/;C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.3.0/../../../;D:/a/msys64/mingw64/lib/
```

C:/msys64で始まるパスはインストールディレクトリからの相対パスです。一方、D:/a/msys64で始まるパスはコンパイル時に指定した絶対パスです。Linuxなどの環境ではコンパイル時に指定したパス以外にインストールされることはあまりないので、相対パスも絶対パスも同じ場所を示しているはずです。一方Windowsでは、MSYS2の環境自体が外様ですので、絶対パスは使用せず相対パスで解決する方式になるはずです。

MSYS2をインストールしていない環境で動作させる場合、先のsearch pathと同じディレクトリを作成し、そこに配置すれば相対パスでの解決が可能になります。しかしもっと良い方法として以下のオプションを指定する方法があります。

```Bash
$ gcc --help
Usage: gcc.exe [options] file...
Options:
  -pass-exit-codes         Exit with highest error code from a phase.
...  
  -B <directory>           Add <directory> to the compiler's search paths.
...  
```

-Bオプションを使用すると、任意のディレクトリをsearch dirに追加することができます。

# 必要ファイルの配置場所
libgccjit-0.dllをemacsのインストールディレクトリのbinに配置してください。emacs.exeが参照できる必要があります。それ以外のファイルは一つのディレクトリにまとめてください。ここを-Bオプションで指定するディレクトリにします。例としてc:/emacs/native-comp/libを使います。

# Native Compilationへの実行オプションの指定。
`native-comp-driver-options`がコンパイル実行時に使用されるオプションの変数です。先ほどの-Bオプションをここで指定します。

オプションには`native-comp-compiler-options`という変数も存在します。JITコンパイルは、.elcからオブジェクトファイル(.o)を作る段階と、オブジェクトファイルをリンクして.dllを作る段階の2段階が存在します。こちらのオプションは.oを作る段階で使用されるオプションの変数です。.aやランタイム用の.oはリンク時に必要となるファイルなので`native-comp-driver-options`にだけオプションを設定します。

以下の内容を初期化ファイルで書けば良いでしょう。ディレクトリはお使いの場所に書き換えてください。

```Lisp
(custom-set-variables
 '(native-comp-driver-options '("-B" "c:/emacs/native-comp/lib" ))
 )
```

ここまで来たならMSYS2の環境は不要です。他の環境は持っていく場合でもlibgccjit-0.dllとc:/emacs/native-comp/lib配下のファイルを一緒にもっていけばNative Compilationが可能です。

# 起動時のエラーへの対応
EmacsのNative Compilationは起動時にlibgccjit-0.dllが読み込めるとオンになるそうです。オンになると`native-comp-available-p`がtを返すようになります。

```Lisp
(native-comp-available-p)
t
```

公式配布のEmacsには、lib/emacs/28.1/native-lisp/28.1-f77b20d2内に配布物の.elnが含まれています。理屈は良く分からないのですが、ここに.elnが存在する.elcも実行環境でコンパイルが行われます。このコンパイル処理は裏で非同期に行われます。\*Async-native-compile-log\*バッファは非同期処理の実行内容がダンプされます。またコンパイルエラーが発生すると\*Warnings\*バッファにエラー内容がダンプされます。

`native-comp-driver-options`の設定は初期化ファイルで行っていますが、初期化ファイルが読み込まれる前にコンパイルが発生すると以下のようなエラーが\*Async-native-compile-log\*に出力されます。

```Lisp
Compiling c:/emacs/share/emacs/28.1/lisp/language/japan-util.el...
ld: dllcrt2.o が見つかりません: No such file or directory
ld: crtbegin.o が見つかりません: No such file or directory
ld: -lmingw32 が見つかりません
ld: -lgcc_s が見つかりません
ld: -lgcc が見つかりません
ld: -lmoldname が見つかりません
ld: -lmingwex が見つかりません
ld: -lmsvcrt が見つかりません
ld: -lmingw32 が見つかりません
ld: -lgcc_s が見つかりません
ld: -lgcc が見つかりません
ld: -lmoldname が見つかりません
ld: -lmingwex が見つかりません
ld: -lmsvcrt が見つかりません
ld: crtend.o が見つかりません: No such file or directory
c:\emacs\bin\libgccjit-0.dll: error: error invoking gcc driver
c:/emacs/share/emacs/28.1/lisp/language/japan-util.el: Error: Internal native compiler error failed to compile

```

-B未指定でリンカが動作しているため、.aや.oを見つけることができないというエラーになっています。コンパイルの処理タイミングを遅らせたりする方法はないので、次回起動時も同様にエラーになってしまいます。これを避けるために事前コンパイルをしてしまいましょう。\*scratch\*で以下のコマンドを実行します。行毎に行末で`C-j`です。

```Lisp
(native-compile "c:/emacs/share/emacs/28.1/lisp/language/japan-util.el")
```

私の環境では以下のファイルの事前コンパイルが必要でした。もしあなたの環境で他のファイルもエラーになるようなら、同様にコンパイルしてください。

```Lisp
(native-compile "c:/emacs/share/emacs/28.1/lisp/language/japan-util.el")
(native-compile "c:/emacs/share/emacs/28.1/lisp/emacs-lisp/cl-lib.el")
(native-compile "c:/emacs/share/emacs/28.1/lisp/emacs-lisp/seq.el")
(native-compile "c:/emacs/share/emacs/28.1/lisp/emacs-lisp/gv.el")
(native-compile "c:/emacs/share/emacs/28.1/lisp/emacs-lisp/cconv.el")
(native-compile "c:/emacs/share/emacs/28.1/lisp/emacs-lisp/bytecomp.el")
```
# エラーメッセージが環境依存していたので追記
私はcygwinを使っているのですが、上記エラーメッセージ、どうもcygwinのas.exeやld.exeが動いた結果、出力されたエラーメッセージのようです。エラーメッセージが特定環境用の例になっていました。GCC実行環境が全くない環境で設定すると以下のようなエラーになります。

```Lisp
Compiling c:/emacs/share/emacs/28.1/lisp/emacs-lisp/cconv.el...
x86_64-w64-mingw32-gcc-11.3.0: fatal error: cannot execute 'as': CreateProcess: No such file or directory
compilation terminated.
```

as.exeがそもそも見つけらずエラーになるようです。全く別なPCに設定を行っていて気付きました。対応方は先に書いた内容と同じです。事前コンパイルを行ってください。

# Emacs 30.1(追記)
Emacs 30.1がリリースされたので上げました。ついでにMSYS2の環境も最新にしてbinディレクトリに必要なdllをコピーし直すとコンパイルエラーが\*Async-native-compile-log\*バッファに出力されます。どうも必要なdllが足りないようです。

しょうがないのでlddを使って確認していきます。
```Bash
$ ldd /mingw64/bin/libgccjit-0.dll
        ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ff95c730000)
        KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ff95bb20000)
        KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ff95a450000)
        msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ff95c200000)
        ADVAPI32.dll => /c/WINDOWS/System32/ADVAPI32.dll (0x7ff95c450000)
        sechost.dll => /c/WINDOWS/System32/sechost.dll (0x7ff95af90000)
        RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x7ff95c560000)
        bcrypt.dll => /c/WINDOWS/System32/bcrypt.dll (0x7ff95a0e0000)
        libgcc_s_seh-1.dll => /mingw64/bin/libgcc_s_seh-1.dll (0x7ff90e850000)
        libgmp-10.dll => /mingw64/bin/libgmp-10.dll (0x7ff90c110000)
        libstdc++-6.dll => /mingw64/bin/libstdc++-6.dll (0x7ff90bec0000)
        libmpfr-6.dll => /mingw64/bin/libmpfr-6.dll (0x7ff90be00000)
        libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff9517f0000)
        libmpc-3.dll => /mingw64/bin/libmpc-3.dll (0x7ff90e820000)
        libisl-23.dll => /mingw64/bin/libisl-23.dll (0x7ff90bbd0000)
        zlib1.dll => /mingw64/bin/zlib1.dll (0x7ff90e7f0000)
        libzstd.dll => /mingw64/bin/libzstd.dll (0x7ff90baa0000)
        CRYPTBASE.DLL => /c/WINDOWS/SYSTEM32/CRYPTBASE.DLL (0x7ff9596f0000)
        bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x7ff959fa0000)
$
```

MSYS2の実行環境はcygwinのようにUNIXのシステムコールをラップするdllが無くて最終的にはWindows標準のdllが使用されます。上記表示の/c/WINDOWS/で始まるdllはWindows標準のdllです。一方、/mingw64で表示されるdllはMSYS2内で相互依存しているdllでbinディレクトリに別途コピーが必要となります。やっかいなのが、MSYS2側のdllがさらに別なdllに依存している場合があることです。

lddは指定されたdllの依存関係しか表示しないので、他のdllの依存関係は個別に確認していく必要があります。昔lddのソースの読んだ時は再帰的に処理するのは将来実装みたいなコメントがありましたが、今時点でも実装されていないので、多分無理なんでしょう。lddの出力結果から再帰的にlddを実行しまくる<a href="https://gist.github.com/tianrui-wei/6b4e643f7800f9ae52739bbc77e185f9">perlスクリプト</a>などもあるのですがパスがLinuxなどを前提にしているのでMSYS2では動作しません(直せば動作するような気もしますが)。なので一個一個調べていきます。


```Bash
$ ldd /mingw64/bin/libgcc_s_seh-1.dll
        ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ff95c730000)
        KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ff95bb20000)
        KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ff95a450000)
        msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ff95c200000)
        libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff933e00000)
        CRYPTBASE.DLL => /c/WINDOWS/SYSTEM32/CRYPTBASE.DLL (0x7ff9596f0000)
        bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x7ff959fa0000)
$
```

libgcc_s_seh-1.dllに関しては他に依存していないようです。

```Bash
$ ldd /mingw64/bin/libmpfr-6.dll
        ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ff92fd90000)
        KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ff92ea70000)
        KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ff92d530000)
        msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ff92ef10000)
        libgmp-10.dll => /mingw64/bin/libgmp-10.dll (0x7ff8ea790000)
        libgcc_s_seh-1.dll => /mingw64/bin/libgcc_s_seh-1.dll (0x7ff8ea760000)
        libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7ff8ea740000)
        CRYPTBASE.DLL => /c/WINDOWS/SYSTEM32/CRYPTBASE.DLL (0x7ff92ccf0000)
        bcryptPrimitives.dll => /c/WINDOWS/System32/bcryptPrimitives.dll (0x7ff92d4a0000)
$
```

libmpfr-6.dllは、libgmp-10.dllなど依存しているdllは存在しますがlibgccjit-0.dllでも出力されているので、libgccjit-0.dllの結果からコピーすれば問題なさそうです。

こんな感じで地味に確認してきます。面倒臭いので自動化したいなぁとも思うのですが、それほど頻繁にやることでもないので、結局手作業のままになっています。

確認の結果、libgccjit-0.dllに関しては問題はないのことがわかりました。他にコピーしたバイナリとして、as.exeやld.exeがあります。試しにas.exeを確認してみましょう。

```Bash
$ ldd as.exe
        ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7fff16850000)
        KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7fff14cc0000)
        KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7fff140f0000)
        msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7fff15630000)
        libintl-8.dll => /mingw64/bin/libintl-8.dll (0x7fff02be0000)
        ADVAPI32.dll => /c/WINDOWS/System32/ADVAPI32.dll (0x7fff15e70000)
        sechost.dll => /c/WINDOWS/System32/sechost.dll (0x7fff149f0000)
        RPCRT4.dll => /c/WINDOWS/System32/RPCRT4.dll (0x7fff156d0000)
        bcrypt.dll => /c/WINDOWS/System32/bcrypt.dll (0x7fff14420000)
        libwinpthread-1.dll => /mingw64/bin/libwinpthread-1.dll (0x7fff0f250000)
$
```

libintl-8.dllは、どこにも出てきていないdllです。結局、このファイルとこれがさらに依存しているlibiconv-2.dllが足りないのが分かりコピーしたら動作しました。

この状態で暫く運用したのですが、<a href="#起動時のエラーへの対応">起動時のエラーへの対応</a>で出力されるエラーが結構頻繁に*Async-native-compile-log\*バッファに出力されます。設定が効いてないのかと思って手動で`native-compile`すると正常にコンパイルできます。非同期コンパイルする時だけ`native-comp-driver-options`設定が効いてないようです。

非同期コンパイルのlispソースをデバッガで覗いていみたのですが、非同期コンパイルはコンパイル用のワーカープロセスみたいのを起動するようです。その際、Native Compilation用の変数(上の`native-comp-driver-options`など)は`setq`してワーカープロセスに渡しているようでした。

`custom-set-variables`で設定するカスタマイズ変数と通常の変数の違いは変数にカスタマイズ用のプロパティがあるかどうかだけというのをelispのマニュアルかどこかで読んだので、あんまり関係ないじゃないかと思ったのですが、初期化ファイルの内容を以下ようにように書き換えました。

```Lisp
;;(custom-set-variables
;; '(native-comp-driver-options '("-B" "c:/emacs/native-comp/lib" ))
;; )
(setq native-comp-driver-options '("-B" "c:/emacs/native-comp/lib" ))
```

この状態で暫く運用していますが、非同期コンパイルでエラーが出ることは無くなりました。もしかしたら、 レキシカルスコープ関連なのかもしれません。私の初期化ファイルは昔から使っている内容をチョコチョコ直しているので

```Lisp
;;; -*- lexical-binding:t -*-

```

の記述はありません。レキシカルスコープにせず、`setq`すると非同期プロセスのコード設計者の意図通りに動くのかもしれません。<a href="https://qiita.com/waiseiningenchokon/items/faca52752c2f78284ecd">こっち</a>に書いてますが、私micro lisperなので、これ以上の原因追及には、時間が掛かりそうなのと、うまく動いてはいるので、このまま運用することにしました。

# あとがき

昔、新世紀エヴァンゲリオンの考察同人誌というのを読んだことがあって、”あとがき”みたいな部分に、”この本を書く上でいろいろなことを調べた(死海文書?とか)。異論や反論があるなら、最低限自分と同じレベルの調査を行ってからじゃないと聞く耳持たない。”というようなことが書いてあって同人誌っていうのは凄い世界だなぁと思いました。