2022-04-06

Windows上のEmacs 28.1でNative Compilationを使う

(2024-02-19追記: 手っ取り早く使い方を知りたい人はMS-Windows版 Emacs 29.1への移行作業をご覧下さい)

Emacs 28.1がリリースされましたね。私は少し前にpretestをいじりましたがすぐに27.2に戻ったので28系を本格的に使うのはようやくこれからです。

Windows版が見当たらないので自分でビルドしようかなーと思っていたらすぐにアップロードされたのでそれ(emacs-28.1.zip)をダウンロードして試してみました。

Emacs 28と言えば大分前からネイティブコンパイルという単語をあちこちで見かけていました。WindowsでGCCに依存した機能って大変なんじゃないの? と思っていたので期待はしていなかったのですが、一応どうなっているのか調べてみることにしました。

Native Compilation (GNU Emacs Lisp Reference Manual)

ええと何々

Emacsのカレントプロセスでネイティブコンパイル済みLispコードの生成およびロードが可能かどうか判断するには、native-comp-available-p (Native-Compilation Functionsを参照)を呼び出してください。

(native-comp-available-p)
nil

あー、やっぱり……。そもそも emacs-28.1.zip の中にGCCなんて入ってないですもんね。

ええと、ネイティブコンパイル機能はlibgccjitを利用していてビルド時に --with-native-compilation の指定が必要。MSYS2にはlibgccjitパッケージもちゃんとあるんですね。

仕方ない、ビルドしますか。以前ビルドした時のMSYS2環境が残っていますし。MSYS2のEmacsパッケージ(mingw-w64-x86_64-emacs)もまだ27.2みたいですし。

MSYS2でEmacs 28.1のビルド

(2022-04-07追記: 公式配布のWindows版はどうやら --with-native-compilation 付きでビルドされているようです。ただし必要なDLLが含まれていないので (native-comp-available-p)nil になります。MSYS2のmingw64/bin/libgccjit-0.dllをEmacsのbinへコピーするかmingw64/binへPATHを通したら (native-comp-available-p)t になりました。なので以下のビルドはしなくても行けます。ただしmingw64/binへ常にPATHを通したくない場合は色々と調整する必要があります(後述))

nt/INSTALLnt/INSTALL.W64等を見るにビルド方法に大きな変更は無さそうです。

まずはMSYS2から既存パッケージのアップグレード(sourceforgeのミラーが廃止されたんですね。/etc/pacman.d以下を少し修正)。

その後今回追加で必要になるであろうmingw-w64-x86_64-libgccjitをインストール。

pacman -S mingw-w64-x86_64-libgccjit

次にMinGW64環境からビルド。 --without-dbus--with-gnutls は前にビルドした時に付けていたので一応。今も必要なのか不明。 --with-native-compilation は無いとダメだったので付けました。 --prefix にはインストール先を明示的に指定します。後でMinGW64環境と分離したいので。

mkdir 20220406 && cd 20220406
wget https://ftp.gnu.org/gnu/emacs/emacs-28.1.tar.gz
tar xvfz emacs-28.1.tar.gz
./configure --without-dbus --with-gnutls --with-native-compilation --prefix=/c/hogehoge/emacs-28.1
make -j4 #4はコア数
make install-strip
/c/hogehoge/emacs-28.1/bin/runemacs.exe -Q # 試しに起動
(native-comp-available-p)
t

うん、大丈夫っぽい。というか裏で自動的にコンパイルが走っててキモいんですけど(笑)

MSYS2 MinGW環境からの分離

次にMSYS2 MinGW環境以外からでも単独で起動できるように必要なDLL等をemacsのディレクトリへコピーします。

これで runemacs.exe をエクスプローラ等から直接実行しても起動するようになりますが……やはりネイティブコンパイルでエラーが出まくります。libgccjitがGCCを見つけられないのですから当たり前ですよね。というかCygwinのが起動されてるっぽい? ldがライブラリが見つからないとか言ってます。私はCygwinの方にはPATHを通してしまっていますからね。Cygwinのldが起動されて、それがMinGWのライブラリが見つけられないという流れになってしまっているのかもしれません。

Internals — libgccjit 12.0.1 (experimental ) documentation あたりを見ると、PATH、LIBRARY_PATHあたりの環境変数をちゃんとすれば何とかなりそう。

試しに (setenv "PATH" (concat "c:/hogehoge/msys/mingw64/bin;" (getenv "PATH"))) を評価してPATHの先頭にMinGWのbinを追加したところコンパイルが成功するようになりました。

うーん、でも常時MinGWにPATHを通したくないんだよなぁ……。

native-comp-async-env-modifier-form という変数があったので、そこで環境変数を設定してみるもうまくいかず。

ネイティブコンパイルは外部のemacsを起動して行う(コンパイル用のelファイルを生成して、 emacs --batch -l ????.el を起動する)みたいなので、その起動時にだけ環境変数を追加することにしました。

(when (and (fboundp #'native-comp-available-p)
           (native-comp-available-p))
  (defun my-comp-set-env (orig-fun &rest args)
    (let ((process-environment
           (append
            (list (concat "PATH=c:/hogehoge/msys/mingw64/bin;" (getenv "PATH"))
                  ;;"LIBRARY_PATH=c:/hogehoge/msys/mingw64/lib" 不要っぽい
                  )
            process-environment)))
      (apply orig-fun args)))

  (require 'comp)
  (advice-add #'comp-final :around #'my-comp-set-env) ;; call-processでemacsを起動している
  (advice-add #'comp-run-async-workers :around #'my-comp-set-env)) ;;make-processでemacsを起動している

適当ですがとりあえず。これで直接runemacs.exeを起動したとき(PATHにmingw64/binが含まれていないとき)でもネイティブコンパイルが成功するようになりました。

で、 ~/.emacs.d/eln-cache/ に色々ファイルが生成されているわけですが、これってちゃんと使われているのでしょうか。スピードも上がっているのでしょうか。後で調べてみようっと。

(2022-04-08追記:)

28.0.91(pretest)のWindows用バイナリが出たときのredditの投稿に、参考になることが色々書いてありました。

alpha.gnu.org has shiny new Emacs 28.0.91 Windows binaries : emacs

中でも、MSYS2から17ファイルほどコピーすればMSYS2無しでもネイティブコンパイルが出来るのだとか。

  • bin/libgccjit-0.dll
  • lib/gcc/crtbegin.o
  • lib/gcc/crtend.o
  • lib/gcc/dllcrt2.o
  • lib/gcc/libadvapi32.a
  • lib/gcc/libgcc.a
  • lib/gcc/libgcc_s.a
  • lib/gcc/libkernel32.a
  • lib/gcc/libmingw32.a
  • lib/gcc/libmingwex.a
  • lib/gcc/libmoldname.a
  • lib/gcc/libmsvcrt.a
  • lib/gcc/libpthread.a
  • lib/gcc/libshell32.a
  • lib/gcc/libuser32.a
  • libexec/emacs/28.0.91/x86_64-w64-mingw32/ld.exe
  • libexec/emacs/28.0.91/x86_64-w64-mingw32/as.exe

MSYS2の中からファイルを探してemacs-28.1.zipを展開したディレクトリ下の上記の場所にコピーして試したところ、確かに関数の同期コンパイルくらいは出来ているように見えます(私のPCはCygwinのldにPATHが通ってたりするので、他のPCで再現しない可能性あり。後でちゃんと確認します)。非同期のコンパイルはPATHを調整しないとダメだとも書いてあります。

後でもう少し詳しく調べて追記します。

(2022-04-11追記: 別の記事として書きました)

Windows上のEmacs 28.1でネイティブコンパイルする方法(まとめ)