はじめに
前回の投稿でソースコードをまとめて実行アーカイブ化するツールの紹介をしました.今回ご紹介するのはHyをネイティブコンパイルし実行ファイルや共有ライブラリを作成するツールです.ついでにPythonやC言語にも変換できます.HyCCという名前でPyPIに登録しています.ただし予期せぬバグの可能性がまだまだ有りますので使用は自己責任でおねがいします.ちなみに全てHyで実装しています.
使い方
インストールはpip
からどうぞ.
ただしWindows環境での動作は未確認です.
$ pip install hycc
なお,PyPIの最新版のHy(v0.12.1)だといくつかのコードでHyのバグ起因のエラーが発生することがあるのでGithubから最新版をインストールしておいてください.
2017/07/01追記:HyおよびHyCCのアップデートによりこの作業は不要になりました.
準備は以上ですのでここから以下のコードを例にとって説明します.
(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
は以下のように使用できます.
(import hello)
(hello.hello)
; >hello!
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における無効な識別子とは以下のとおりです.
- 0-9の数字で始まる識別子
- _, a-z, A-Z, 0-9以外の文字を含む識別子
以下のコードを例にとります.
(reduce + [1 2 3])
; > 6
これをhy2py
で変換すると以下のようになります.
from hy.core.language import reduce
from hy.core.shadow import +
# +は無効な識別子
reduce(+, [1, 2, 3])
ここで+
は先に挙げた2.に該当するのでダメというわけです.これを単純に別の有効な名前,例えば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コードを生成します.
import hy.core.language as _
reduce = _.getattr("reduce")
import hy.core.shadow as _
+ = _.getattr("+")
reduce(+, [1, 2, 3])
このコードはhy2py
が吐くコードと等価ですが+
を任意の名前で置換しても問題ありません.HyCCではASTレベルでのアクセスとソースコードレベルでのアクセスを上手く組み合わせることでエラーを回避しているわけです.同様にオブジェクトのメンバアクセスもgetattr
とsetattr
で書き換えています.
HyCC使用時の注意(重要)
先ほど触れていたHyCC使用時の注意についてです.ここまでで説明したようにHyCCでは無効な識別子を工夫を交えつつ有効なものへと置換しています.この際,一点だけ副作用というか問題が発生してしまいます.
(def hoge/fuga 0)
(print (get (globals) "hoge/fuga"))
; > 0
上記のコードはHyCCにより以下のようなPythonコードに変換されます.
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'
おわかりいただけたでしょうか.無効な識別子を有効なものに置換する弊害としてglobals
やlocals
,inspect
モジュールなどが場合によっては正しく動かなくなってしまいます.
この問題に対する対策はいくつか検討中ですが,そもそもHy自体が同様の問題をすでに抱えています.Hyでは構文解析が走る段階でhoge!
がhoge_bang
に,hoge?
がis_hoge
にそれぞれ置き換えられます.従って以下のコードは正しく動きません.
(def hoge! 0)
(print (get (globals) "hoge!"))
; > KeyError!
従って,globals
などの利用はHyCC使用時の注意というよりはHy使用時の注意ともいえます.
2017/06/04追記
アップデートで対応しました!具体的にはglobals
やlocals
を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はまだまだ発展途上のクソマイナー言語と言った感じですがユーザーが増えて議論が活発になることでより洗練されていくことを期待しています.
ここまで読んでいただきありがとうございました.