前回 は、簡単な Python の Webアプリケーションとして、WSGI を使った、wsgiref、Werkzeug、Flask を動かしてみました。
今回は、Flask で使った Python ファイルを Cython化してみたいと思います。Cython化するメリットは、Pythonの高速化が大きいですが、セキュリティ的に分かりにくいソースコードになるという点もあるようです。
それでは、やっていきます。
はじめに
「セキュリティ」の記事一覧です。良かったら参考にしてください。
セキュリティの記事一覧
開発環境は、VirtualBox + ParrotOS 6.1 です。
Cython化する方法
Cython化する方法は、いくつかあるようです。ウィキペディアにまとまってると助かったのですが、残念ながら内容が薄かったです。仕方ないので、公式サイトに行きます。
cython.org
- 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.readthedocs.io
cythonのインストール
まず、Cython をインストールします。pip で簡単にインストールできました。
$ pip install cython
Successfully installed cython-3.0.10
setup.pyを使った方法でCython化する
参考にする Cython の公式ドキュメントのチュートリアルです。
cython.readthedocs.io
本来は、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 の部分と思われるところだけ貼ります。さすがに、人間が読むのは、しんどそうです。
static PyObject *__pyx_pw_5hello_1hello_world(PyObject *__pyx_self, CYTHON_UNUSED PyObject *unused);
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);
__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);
__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;
__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」で囲ったものでした。興味深いです(笑)。
最後になりましたが、エンジニアグループのランキングに参加中です。
気楽にポチッとよろしくお願いいたします🙇
今回は以上です!
最後までお読みいただき、ありがとうございました。