前回 は、簡単な Python の Webアプリケーションとして、WSGI を使った、wsgiref、Werkzeug、Flask を動かしてみました。
今回は、Flask で使った Python ファイルを Cython化してみたいと思います。Cython化するメリットは、Pythonの高速化が大きいですが、セキュリティ的に分かりにくいソースコードになるという点もあるようです。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる ← 今回
開発環境は、VirtualBox + ParrotOS 6.1 です。
Cython化する方法
Cython化する方法は、いくつかあるようです。ウィキペディアにまとまってると助かったのですが、残念ながら内容が薄かったです。仕方ないので、公式サイトに行きます。
- setup.py を作り、cythonizeメソッドの引数に、pyxファイルを渡しておき、setup.py を実行することで、soファイルが生成させる方法
- pyxファイルに対して、cythonizeコマンドを使ってコンパイルすることで、soファイルが生成させる方法
Cython化してみる
Cython化の対象は、前回 作成した hello.py とします。
Cython化する場合、ファイルの拡張子は pyx にする必要があるようなので、hello.py をコピーして、hello.pyx としました。ファイルの中身は同じです。
from flask import Flask app = Flask(__name__) @app.route("/") def hello_world(): return "<p>Hello, World!</p>"
Cython化する方法は、以下の公式ドキュメントを参考にやっていきます。
cythonのインストール
まず、Cython をインストールします。pip で簡単にインストールできました。
$ pip install cython Successfully installed cython-3.0.10
setup.pyを使った方法でCython化する
参考にする Cython の公式ドキュメントのチュートリアルです。
本来は、hello.pyx の中身を Cython化のメリットが出るように、書き換える方がいいのだと思いますが、まずは、中身を変えずにやってみます。
setup.py が必要なので作ります。チュートリアルの通りに作ります。
from setuptools import setup from Cython.Build import cythonize setup( ext_modules = cythonize( ['hello.pyx'] ) )
では、早速 setup.py を実行して、ビルドしてみます。
$ python setup.py build_ext --inplace running build_ext building 'hello' extension creating build creating build/temp.linux-x86_64-cpython-311 x86_64-linux-gnu-gcc -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -fPIC -I/home/user/20240731_flask/include -I/usr/include/python3.11 -c hello.c -o build/temp.linux-x86_64-cpython-311/hello.o creating build/lib.linux-x86_64-cpython-311 x86_64-linux-gnu-gcc -shared -Wl,-O1 -Wl,-Bsymbolic-functions -g -fwrapv -O2 build/temp.linux-x86_64-cpython-311/hello.o -L/usr/lib/x86_64-linux-gnu -o build/lib.linux-x86_64-cpython-311/hello.cpython-311-x86_64-linux-gnu.so copying build/lib.linux-x86_64-cpython-311/hello.cpython-311-x86_64-linux-gnu.so ->
どういうファイルが作られたか、見てみます。
hello.pyx と同じ階層に、hello.c というCソースコードと、soファイルが作られました。それとは別に、buildディレクトリが作られて、その中に、オブジェクトファイル(hello.o)と、hello.pyx と同じ階層の soファイルと同じファイル(diffコマンドで確認済み)が置かれています。
$ tree . |-- __pycache__ | `-- hello.cpython-311.pyc |-- build | |-- lib.linux-x86_64-cpython-311 | | `-- hello.cpython-311-x86_64-linux-gnu.so | `-- temp.linux-x86_64-cpython-311 | `-- hello.o |-- hello.c |-- hello.cpython-311-x86_64-linux-gnu.so |-- hello.py |-- hello.pyx `-- setup.py 5 directories, 8 files
では、実行してみます。
同じディレクトリに hello.py があると、どちらが実行されたか分からなくなるので、exeディレクトリを新しく作り、そこで実行してみて、実行できないことを確認します。その後、そこに soファイルのシンボリックファイルを作って、そこで実行してみます。
$ mkdir exe $ cd exe/ $ flask --app hello run Usage: flask run [OPTIONS] Try 'flask run --help' for help. Error: Could not import 'hello'. $ ln -s ../hello.cpython-311-x86_64-linux-gnu.so $ tree ../ ../ |-- __pycache__ | `-- hello.cpython-311.pyc |-- build | |-- lib.linux-x86_64-cpython-311 | | `-- hello.cpython-311-x86_64-linux-gnu.so | `-- temp.linux-x86_64-cpython-311 | `-- hello.o |-- exe | `-- hello.cpython-311-x86_64-linux-gnu.so -> ../hello.cpython-311-x86_64-linux-gnu.so |-- hello.c |-- hello.cpython-311-x86_64-linux-gnu.so |-- hello.py |-- hello.pyx `-- setup.py 6 directories, 9 files $ flask --app hello run * Serving Flask app 'hello' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit $ sudo lsof -i | grep user flask 2359 user 3u IPv4 51036 0t0 TCP localhost:5000 (LISTEN)
意図通り、soファイルがカレントディレクトリにあるだけで、Flask の起動が出来ました。
参考までに、作成された hello.c を見ておきます。6391行(239KB)もあるので、hello.py の部分と思われるところだけ貼ります。さすがに、人間が読むのは、しんどそうです。
/* #### Code section: module_code ### */ /* "hello.pyx":5 * app = Flask(__name__) * * @app.route("/") # <<<<<<<<<<<<<< * def hello_world(): * return "<p>Hello, World!</p>" */ /* Python wrapper */ static PyObject *__pyx_pw_5hello_1hello_world(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused); /*proto*/ static PyMethodDef __pyx_mdef_5hello_1hello_world = {"hello_world", (PyCFunction)__pyx_pw_5hello_1hello_world, METH_NOARGS, 0}; static PyObject *__pyx_pw_5hello_1hello_world(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused) { CYTHON_UNUSED PyObject *const *__pyx_kwvalues; PyObject *__pyx_r = 0; __Pyx_RefNannyDeclarations __Pyx_RefNannySetupContext("hello_world (wrapper)", 0); __pyx_kwvalues = __Pyx_KwValues_VARARGS(__pyx_args, __pyx_nargs); __pyx_r = __pyx_pf_5hello_hello_world(__pyx_self); /* function exit code */ __Pyx_RefNannyFinishContext(); return __pyx_r; } static PyObject *__pyx_pf_5hello_hello_world(CYTHON_UNUSED PyObject *__pyx_self) { PyObject *__pyx_r = NULL; __Pyx_RefNannyDeclarations __Pyx_RefNannySetupContext("hello_world", 1); /* "hello.pyx":7 * @app.route("/") * def hello_world(): * return "<p>Hello, World!</p>" # <<<<<<<<<<<<<< */ __Pyx_XDECREF(__pyx_r); __Pyx_INCREF(__pyx_kp_s_p_Hello_World_p); __pyx_r = __pyx_kp_s_p_Hello_World_p; goto __pyx_L0; /* "hello.pyx":5 * app = Flask(__name__) * * @app.route("/") # <<<<<<<<<<<<<< * def hello_world(): * return "<p>Hello, World!</p>" */ /* function exit code */ __pyx_L0:; __Pyx_XGIVEREF(__pyx_r); __Pyx_RefNannyFinishContext(); return __pyx_r; }
cythonizeコマンドを使う方法でCython化する
先ほどと同じディレクトリで実行すると、ファイルが混ざってしまいそうなので、別のディレクトリを作り、hello.pyx だけをコピーしておきます。
では、早速 cythonizeコマンドを実行してみます。-a オプションはソースコードの注釈付きの HTMLファイルが生成されます。-i オプションは --inplace と同じで、このディレクトリに soファイルが生成されます。
$ cythonize -a -i hello.pyx Compiling /home/user/svn/flask/tutorial2/hello.pyx because it changed. [1/1] Cythonizing /home/user/svn/flask/tutorial2/hello.pyx /home/user/20240731_flask/lib/python3.11/site-packages/Cython/Compiler/Main.py:381: FutureWarning: Cython directive 'language_level' not set, using '3str' for now (Py3). This has changed from earlier releases! File: /home/user/svn/flask/tutorial2/hello.pyx tree = Parsing.p_module(s, pxd, full_module_name)
成功しました。出力されたメッセージは、先ほどの setup.py を使った方法とは、全く異なりますね。setup.py を使った方法の方が、コンパイルしてる感じでした。
では、生成されたファイルを確認してみます。
$ tree . |-- build | `-- lib.linux-x86_64-cpython-311 | `-- hello.cpython-311-x86_64-linux-gnu.so |-- hello.c |-- hello.cpython-311-x86_64-linux-gnu.so |-- hello.html `-- hello.pyx 3 directories, 5 files
先ほどとの違いは、オブジェクトファイルが出力されていないことと、HTMLファイルが生成されていることでしょうか。先ほどと同様に、soファイルに差異はありませんでした。
hello.c を比べてみました。ファイルパスの文字列だけが異なっているだけで、それ以外は同じソースコードが生成されたようです。
--- hello.c 2024-07-31 22:55:56.083132149 +0900 +++ ../tutorial/hello.c 2024-07-31 22:11:51.724087415 +0900 @@ -5,7 +5,7 @@ "distutils": { "name": "hello", "sources": [ - "/home/user/svn/flask/tutorial2/hello.pyx" + "hello.pyx" ] }, "module_name": "hello"
では、実行してみます。
先ほどと同様に、空のディレクトリを作って、その中で実行します。
$ mkdir exe $ cd exe/ $ flask --app hello run Usage: flask run [OPTIONS] Try 'flask run --help' for help. Error: Could not import 'hello'. $ ln -s ../hello.cpython-311-x86_64-linux-gnu.so $ tree ../ ../ |-- build | `-- lib.linux-x86_64-cpython-311 | `-- hello.cpython-311-x86_64-linux-gnu.so |-- exe | `-- hello.cpython-311-x86_64-linux-gnu.so -> ../hello.cpython-311-x86_64-linux-gnu.so |-- hello.c |-- hello.cpython-311-x86_64-linux-gnu.so |-- hello.html `-- hello.pyx 4 directories, 6 files $ flask --app hello run * Serving Flask app 'hello' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit $ sudo lsof -i | grep user flask 79983 user 3u IPv4 241061 0t0 TCP localhost:5000 (LISTEN)
先ほどと同じように実行できているようです。
おわりに
今回は、少し寄り道をして、PythonスクリプトをCython化してみました。
Cython のロゴは、Python のロゴを「C」で囲ったものでした。興味深いです(笑)。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。