更新日から1年以上経過しています。情報が古い可能性がございます。
先日、Remote.pyというイベントでLT登壇してきました。
テーマは、「poetry時代のC extensionの作り方」で。
そこで発表した内容をブログにもまとめておきます。
やり方だけ知りたい方はLT用にGithubにリポジトリも用意しておいたので、そちらでお試しください。
https://github.com/Lucky-Mano/Poetry_C_Extension_Example
Poetry とは
Poetry になじみのない方もいるかと思うので、簡単に Poetry の説明をしておきます。
Poetry は、Python のパッケージ管理ツールの一つです。pipenv の方が有名なので、そちらの方を使っている方も多いかもしれません。
それぞれ、以下のサイトをご覧ください。
どちらも公式ドキュメントが充実しています。
Poetry – Python dependency management and packaging made easy.
Pipenv: Python Dev Workflow for Humans
Poetry で C extension を開発する際の問題
以下の一点に集約されます。
- setup.py が無い
このために、Poetry では build.py を用意し、build の際にはそれを使うよう pyproject.toml に記述をしておく必要があります。
[tool.poetry]
...
build = "build.py"
build.py 自体は setuptools を利用して以下のように用意しておきます。プロジェクトの全体は Github をご覧ください。
from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError
from setuptools import Extension
from setuptools.command.build_ext import build_ext
extensions = [
Extension("spam.ext", sources=["ext/src/spammodule.c"]),
Extension("spam.math.cmath", sources=["ext/src/mathmodule.c"]),
]
class BuildFailed(Exception):
pass
class ExtBuilder(build_ext):
def run(self):
try:
build_ext.run(self)
except (DistutilsPlatformError, FileNotFoundError):
pass
def build_extension(self, ext):
try:
build_ext.build_extension(self, ext)
except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError):
pass
def build(setup_kwargs):
setup_kwargs.update({"ext_modules": extensions, "cmdclass": {"build_ext": ExtBuilder}})
上記のコードで、extensions という名前のリストに入っているのが C extension になります。
第一引数にビルドされた .so を配置する場所を、sources で実際のソースコードを指定します。sources で指定したコードが include するヘッダー等を必要とするなら、include_dirs でディレクトリを指定します。
C のコード
Poetry とは直接関係ないのですが、いくつか気を付ける箇所があります。その中のうちの一つを。
...
static struct PyModuleDef spammodule = {PyModuleDef_HEAD_INIT, "ext", NULL, -1, SpamMethods};
PyMODINIT_FUNC PyInit_ext(void)
{
return PyModule_Create(&spammodule);
}
spammodule.c の最後の部分を抜粋していますが、まず PyModuleDef に与えている構造体の第2引数の ext と、PyMODINIT_FUNC の関数名の ext の部分は一致させる必要があります。公式ドキュメントにもきちんと記載があるのですが、ここを忘れるとモジュールが見つからない、と言われます。
処理としては、.so ファイルに import 等でアクセスした際に PyInit_ext が呼ばれ、Python 側から C extension が見えるようになります。この際に名前がズレていると対象のモジュールが見つからない、という事態になります。
C でモジュールを開発する必要性
これはぶっちゃけないです。LT でも話したのですが、PyPI には多種多様なライブラリが既に公開されています。あなたが C extension として使いたいと思っている OSS も、多分既に既に誰かが PyPI に登録しています。
それ、既存ライブラリでできるよ!と言い切れるようにどんなものがあるか、常にアンテナを張っておきましょう!
それでも C extension を開発せざるを得ない時には、私の記事が役に立つことを祈っております。
余談
これは LT では口頭で軽く触れただけなのですが、Objective-C で書かれたコードも C extension としてビルドできます。macOS の OS 側のコードを使った C extension を開発したい場合に使えます。(うまくやれば Swift でも C extension が作れる気がしますが、試してないのでコソコソと述べておきます。)
macOS 用の C extension を作る際には、build.py に以下の記述が必要になることがあります。
os.environ['LDFLAGS'] = '-framework Foundation'
macOS での開発に詳しい方ならこれで大体分かってくれるかと。
まとめ
Python 等の上位層しか見なくて良いプログラムを書いているはずなのに、どうしても C/C++ を書かざるを得ない機会がままあります。我々はまだまだ C/C++ から逃れられないのだ、と言う気持ちで、みなさんも C/C++ を書きましょう。低レイヤー楽しいよ!!