Python
lisp
hy

Hyをネイティブコンパイルするツールを作った

More than 1 year has passed since last update.


はじめに

前回の投稿でソースコードをまとめて実行アーカイブ化するツールの紹介をしました.今回ご紹介するのはHyをネイティブコンパイルし実行ファイルや共有ライブラリを作成するツールです.ついでにPythonやC言語にも変換できます.HyCCという名前でPyPIに登録しています.ただし予期せぬバグの可能性がまだまだ有りますので使用は自己責任でおねがいします.ちなみに全てHyで実装しています.


使い方

インストールはpipからどうぞ.

ただしWindows環境での動作は未確認です.

$ pip install hycc

なお,PyPIの最新版のHy(v0.12.1)だといくつかのコードでHyのバグ起因のエラーが発生することがあるのでGithubから最新版をインストールしておいてください.

2017/07/01追記:HyおよびHyCCのアップデートによりこの作業は不要になりました.

準備は以上ですのでここから以下のコードを例にとって説明します.


hello.hy

(defn hello []

(print "hello!"))

(defmain [&rest args]
(hello))



実行ファイルを作成する

$ hycc hello.hy

これでカレントディレクトリにhelloという実行バイナリが作成されます.

$ ./hello

hello!


共有ライブラリを作成する

--sharedオプションつきでビルドします.

$ hycc hello.hy --shared

これでカレントディレクトリにhello.soという共有オブジェクトが作成されます.

Pythonは共有オブジェクトをモジュールとしてインポートする機能が有りますがHyももちろん同様です.

従ってこのhello.soは以下のように使用できます.


Hyからhello.soをインポート

(import hello)

(hello.hello)
; >hello!


Pythonからhello.soをインポート

import hello

hello.hello()
# >hello!

ネイティブコンパイルされたモジュールや実行ファイルはだいたい2〜8倍ぐらい高速化されます.なのでhycを用いて.pycファイルにバイトコンパイルするよりも有用かと思います.またもちろんPythonよりも高速です.なお内部ではCythonを用いていますが速さのイメージとしてはだいたい以下のような感じです.

C < Cython(型指定有り) << Cython(型指定なし) < HyCC < Python < Hy

概ねhycの上位互換ですが,一点だけ注意が有ります.内部の仕組みに関わるものですので以下より仕組みを説明しつつ解説します.


仕組み

大雑把にいうとHyのコードをPythonに変換しCython経由でCに変換してコンパイルしています.ただし,前回の投稿で触れたように,Hyに標準で付属しているhy2pyを用いて生成されるPythonのコードはそのままでは動きません.従ってhy2pyしてcythonizeすればよいというわけにはいきません.


HyをPythonに変換

そもそもなぜhy2pyの吐くコードが動かないのかというとPythonでは無効な識別子が用いられているからです.Pythonにおける無効な識別子とは以下のとおりです.


  1. 0-9の数字で始まる識別子

  2. _, a-z, A-Z, 0-9以外の文字を含む識別子

以下のコードを例にとります.


sample.hy

(reduce + [1 2 3])

; > 6

これをhy2pyで変換すると以下のようになります.


hy2pyが吐くコード

from hy.core.language import reduce

from hy.core.shadow import +
# +は無効な識別子
reduce(+, [1, 2, 3])

ここで+は先に挙げた2.に該当するのでダメというわけです.これを単純に別の有効な名前,例えばaddに置換しても上手くいきません.


hy2pyが吐くコードの+をaddに置換

from hy.core.language import reduce

from hy.core.shadow import add
# ImportError
reduce(add, [1, 2, 3])

当たり前のことですがhy.core.shadowモジュールにはaddという名前が定義されていないのでImportErrorとなります.HyCCではこの問題を解決するためにHyのソースからまず以下のようなPythonコードを生成します.


HyCCのやり方

import hy.core.language as _

reduce = _.getattr("reduce")
import hy.core.shadow as _
+ = _.getattr("+")
reduce(+, [1, 2, 3])

このコードはhy2pyが吐くコードと等価ですが+を任意の名前で置換しても問題ありません.HyCCではASTレベルでのアクセスとソースコードレベルでのアクセスを上手く組み合わせることでエラーを回避しているわけです.同様にオブジェクトのメンバアクセスもgetattrsetattrで書き換えています.


HyCC使用時の注意(重要)

先ほど触れていたHyCC使用時の注意についてです.ここまでで説明したようにHyCCでは無効な識別子を工夫を交えつつ有効なものへと置換しています.この際,一点だけ副作用というか問題が発生してしまいます.


HyCCでダメなヤツ

(def hoge/fuga 0)

(print (get (globals) "hoge/fuga"))
; > 0

上記のコードはHyCCにより以下のようなPythonコードに変換されます.


HyCCが生成するコード

from __future__ import print_function

import hy
hogex2Ffuga = 0L
print(globals()[u'hoge/fuga'])

いくつかインポートが追加されているのは無視していただいて構いません.もとのHyのコードではhoge/fugaがPythonで無効な名前でした.従ってHyCCの生成するコードではhogex2Ffugaと置換されています.

このコードを実行すると以下のようなエラーとなります.

  File "test.py", line 4, in <module>

print(globals()[u'hoge/fuga'])
KeyError: u'hoge/fuga'

おわかりいただけたでしょうか.無効な識別子を有効なものに置換する弊害としてglobalslocalsinspectモジュールなどが場合によっては正しく動かなくなってしまいます.

この問題に対する対策はいくつか検討中ですが,そもそもHy自体が同様の問題をすでに抱えています.Hyでは構文解析が走る段階でhoge!hoge_bangに,hoge?is_hogeにそれぞれ置き換えられます.従って以下のコードは正しく動きません.


Hyでダメなヤツ

(def hoge! 0)

(print (get (globals) "hoge!"))
; > KeyError!

従って,globalsなどの利用はHyCC使用時の注意というよりはHy使用時の注意ともいえます.

2017/06/04追記

アップデートで対応しました!具体的にはglobalslocalsをdictっぽい謎クラスでラップすることでKeyエラー回避してます.相変わらずinspectモジュールは正しく動きませんがそもそもCython自体がinspectに対応していないのでどうしようも無いというのが本音です.詳しくはこのコミットをご参照ください.


オマケ(ちゃんと動くPythonに変換する)

HyCCにはhy2pyのようにHyからPythonに変換する機能もついています.

$ hycc hello.hy --python

これでカレントディレクトリにhello.pyが出力されます.先述の注意に気を付けていればhy2pyが出力するコードとは違いしっかり動作します.意外とこの機能のほうが需要は高いかもしれません.また同様に--clangオプションでC言語にも変換できますが使い途があるかは微妙です.


既知のバグ(2017/06/13追記)


共有ライブラリのfromインポートが失敗する

githubのissueにも書きましたがPythonの仕様なのかモジュールオブジェクトに対するgettatrでサブモジュールを取得するときにサブモジュールが共有ライブラリを含んでいるとAttributeErrorとなるようです.対応を考え中です.


おわりに

Hyをネイティブコンパイルするツールを紹介しました.開発はすべてgithubで行っていますので気軽にプルリクissueなど投げていただいてかまいません.もちろんここでのコメントも大歓迎です.Hyはまだまだ発展途上のクソマイナー言語と言った感じですがユーザーが増えて議論が活発になることでより洗練されていくことを期待しています.

ここまで読んでいただきありがとうございました.