PyPyで作ったプログラムをcffiでexe化する方法


PyPyは非常に便利なコンパイラ

PyPyはPythonを高速化するための非常に便利なアプリ。
Pythonで動作が遅いな...と思ったり、処理がフリーズした時が有れば、PyPyを使うとスムーズに進む時があります。

PyPyプログラムをexe化する必要性

PyPyを使って開発したアプリは、そのまま公開することが出来ません。
なぜなら、それをインストールした人はPyPyを持っていないと実行出来ないからです。
PythonスクリプトをPyPyを用いてexe化(実行可能ファイル化)することで、すべての人がそのファイルを実行することが出来るようになります。

準備

1. PyPyをインストールする。

公式サイトからダウンロードしてください。

ダウンロードしたPyPyのフォルダはどこに置いても良いですが、その絶対パスは覚えておいてください。

2. 環境変数Pathを通す

Windowsの検索窓で「環境変数」と入力して「環境変数の編集」をダブルクリックしてください。
出てきた画面の「(user名)のユーザー環境変数」内のPathを押して、「新規」をクリックします。
そこに、先ほど覚えた絶対パスを入力してください。

この作業をすることで、どのディレクトリでもpypy3を実行できるようになりました。
(環境変数Pathを通すことでpypy test.pyが実行できるようになる。)
最新バージョンをインストールしたのであれば、pypy3.9も使えます。

3.必要なモジュールをインストール

$pypy -m ensurepip
$pypy -m pip install -U pip

4.clang(LLVM)をインストールする

以下からダウンロードしてください。

cffiを用いてexe化する方法

1. cffiがインストールされているか確認する

cffiはC拡張を作成するためのライブラリです。
PyPyを用いたコンパイルにはこれが必要になります。
PyPyのカレントディレクトリの"Lib"の中にインストールされているか確認しましょう。

今回はC言語を用いてPyPyを実行します。
こうすることでコンパイル(exe化)が容易になります、

2.PyPyでexe化するPythonスクリプトを書く

今回は以下のようなファイルを作りました。

test.py
from pypytest import ffi, lib

@ffi.def_extern()
def pypyrun():
    import platform
    print(platform.python_implementation())

from pypytest import ffi, lib
pypytest.dllからffi,libをインポートして、cffiを利用できるようにします。
まだpypytest.dllは生成していないので、現段階で実行してもエラーが出るでしょう。

@ffi.def_extern()
pypyrun()をcffiで使えるようにするためのデコレータ

platform.python_implementation()
どのPython処理系を使用しているか表示します。
通常のPython処理系を使用している場合は、CPythonと出力され、
PyPy処理系を使用している場合は、PyPyと出力されます。

複雑なスクリプトに見えますが、要は自分の書いたプログラムをpypyrun()の中にそのまま入れたらよいだけです。Pythonでは関数内関数定義も出来るので、非常に簡易であるといえるでしょう。
関数内関数定義とは、def()内でdef()を定義することです。

3.pypyrun()を実行するための埋め込みファイルを作る。

先ほど作成したtest.py内のpypyrun()を実行するためのファイルを作ります。
このファイルはpyファイルではなく、ヘッダファイル(.h)で書き込みます。
C言語からPyPyを実行するからです。

test.h
void pypyrun();

今回は戻り値がないので、voidで関数を実行します。
戻り値がある場合は、その型を指定します。
(例:戻り値が整数ならint pypyrun();)
引数がある場合は、その変数も指定します。
(例:void pypyrun(int apple, int orange)ここでは引数の内容を直接指定しません。)

4.pypyの埋め込み用スクリプトをかく

ここでcffiを使います。

cffitest.py
import cffi
ffibuilder = cffi.FFI()

#pypyrun()を読み込むためのCスクリプト「test.h」を読み込む
with open('test.h') as f:
    ffibuilder.embedding_api(f.read())

#pythonスクリプトを読み込む
with open('test.py') as f2:
    ffibuilder.embedding_init_code(f2.read())

#pypytest.dll(dylib)にプラグイン(埋め込みを)する
ffibuilder.set_source("pypytest", "")

ffibuilder.compile(verbose=True)

ffibuilder.embedding_api()
指定されたC言語を分析し、dllファイルに挿入するコード
ffibuilder.embedding_init_code()
初期のPythonスクリプトを提供するコード
ffibuilder.set_source("pypytest", "")
pypytestを作成するコード

5.mainのCスクリプトを書く

test.c
#include <stdio.h>
#include "test.h"

int main(void) {
    pypyrun();
    printf("successfully completed");

    return 0;
}

#include test.h
pypyrun()を実行するために呼び出します。データはdll(dylib)に含まれます。
pypyrun();
必要な引数が有ればここで指定します。

6.pypyで必要なライブラリを生成

以下を実行してライブラリを生成します。

$pypy3 cffitest.py

Windowsの場合は"Release"というフォルダができ、その中にpypytest.libがあると思います。
同ディレクトリにはpypytest.dllが出来ます。
Macの場合はpypytest.dylibが生成されます。

7.コンパイル

clangを使ってC言語とコンパイルします。

$clang -o test.exe test.c Release/pypytest.lib

(Release内にpypytest.lib(windows用静的ライブラリファイル)がない場合は、別の相対パスを指定してください。)
出来たtest.exeを実行します。

PS C:\Users\xxx\Documents\PyPy_hello\new_test> .\test
PyPy
successfully completed

成功しました!
(同ディレクトリにpypytest.dllがないと実行できません)

8.pypy関連のファイルをコピペ

pypyのファイル内にある"lib-pypy3.9-c.dll"と"libffi-8.dll"、"Lib"をtest.exeと同ディレク内に保存して、別のPCでも実行できるようにする。
(pypytest.dllも必要)
古いバージョンを使っている場合はLibなどがないかもしれません。
必要なディレクトリをすべてコピペしましょう

exe化したファイルでTkinterを使う

test.pyを以下のようなファイルに変更しました。

test.py
from pypytest import ffi, lib

@ffi.def_extern()
def pypyrun():
    import tkinter as tk
    root = tk.Tk()
    root.mainloop()

そしてもう一度上記の手順でビルドして実行すると、以下のようなエラーが出ました。

From cffi callback <function pypyrun at 0x0000011a28c4ac00>:
Traceback (most recent call last):
  File "<init code for 'pypytest'>", line 6, in pypyrun
  File "C:\Users\xxx\Documents\PyPy_hello\new_test\lib-python\3\tkinter\__init__.py", line 2265, in __init__
    baseName = os.path.basename(sys.argv[0])
IndexError: list index out of range
successfully completed

どうやらsys.argvが使えないようです。
対処法として__init__.pyを書き換えます。
2265行目の部分を

baseName = os.path.basename("")

に変更するとエラーはなくなります。
しかし、これではTkWindowは表示されません。Tkinter関連のフォルダ群がないからです。
pypyのカレントディレクトリにある"tcl"というフォルダをコピーして、test.exeと同ディレクトリにペーストすると無事実行できます。

Tkinterを使ったプログラムをコンパイルしたので、Githubに公開しました。
https://github.com/Moyashi-itame/Compile-PyPy-to-the-executable-file-and-use-GUI

コンソール画面を消す

Tkinter等のGUIを使うとコンソール画面が邪魔になりますよね。exe化していないときはpypyw.exeを使って

pypyw test.py

でノーコンソール化出来ますが、exe化しているときは違います。

方法

C言語からPythonを呼び出しているので、test.cのファイルを変更します。

test.c
#include <stdio.h>
#include "test.h"
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){
    pypyrun();
    printf("successfully completed");

    return 0;
}

重要なのは、コンソール画面ではなく、Window画面を使っているということです。
実行可能ファイルをノーコンソール化しているわけではありません。
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
hIntanceアプリケーションやwindowを識別するためのパラメータ
hPrevInstanceWindows16bitの時に使われた古いパラメータ。常にNullが返される
lpCmdLineコマンドラインの文字列
nCmdShowWindowの表示モード

アイコンを変更

Resource Hackerを使うと、exeファイルのアイコンを自由に変更することが出来ます。

容量を減らす

"Lib"フォルダ内の要らないライブラリを削除すると、大幅に容量を削減できますが、
Pythonの標準ライブラリは消さないほうが良いです。
なぜなら、使うライブラリ内で標準モジュールを使用するときがあるからです。
(さらに容量を削減したいなら使用するライブラリのスクリプトを一行ずつ確認して、いらない標準モジュールを判断する必要がある。)

外部ライブラリは使えるか

PyPy7.3.8になってからほぼ全ての外部ライブラリをサポートするになりました。
例えばnumpyやpygame、pandasなど、PyPIにあるものを余すことなくカバーしています。
以下のようなコードを実行してモジュールをインストールしましょう。

$pypy -m pip install numpy

外部ライブラリを使ってみた

せっかくなので実用してみましょう。

test.py
from pypytest import ffi, lib

@ffi.def_extern()
def pypyrun():
    import pygame
    import sys

    def main():
        pygame.init()
        screen = pygame.display.set_mode((800,600))
        clock = pygame.time.Clock()
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            screen.fill((255,255,255))
            pygame.display.update()
            clock.tick(30)
    main()

明らかに成功していますね。

まとめ

pypyを使うメリット

・Cythonよりも実用性が高い
・簡単にexe化出来る。
・ほぼ全てのライブラリをサポートしている(機械学習もできる)
・Cスクリプトから呼び出すことが出来るので、自由度が高い。

デメリット

・モジュールのインストールが遅い。
・容量が大きい

pypyは一見難しく感じるかもしれませんが、慣れると結局はPythonスクリプトを書いているのと同じなので、簡単に利用できます。皆さんもぜひ積極的に使ってみてください。

参考記事

python コード変換なしで高速化 https://www.yukkuriikouze.com/2019/01/12/1497/ 2022年3月28日閲覧
C言語アプリケーションにPyPyを埋め込む https://postd.cc/embedding-pypy-in-a-c-application/ 2022年3月28日閲覧
using CFFI for embedding https://cffi.readthedocs.io/en/latest/embedding.html 2022年3月28日閲覧
pypyでコンパイルしたプログラムを配布したい https://teratail.com/questions/333759?link=qa_related_pc 2022年3月28日閲覧
ゼロから始めるWindowsAPI https://news.mynavi.jp/article/20090525-windowsapi/ 2022年3月28日閲覧