PoetryでC Extensionを開発する

更新日から1年以上経過しています。情報が古い可能性がございます。

先日、Remote.pyというイベントでLT登壇してきました。
テーマは、「poetry時代のC extensionの作り方」で。

Remote.py #2 【オンライン】

そこで発表した内容をブログにもまとめておきます。
やり方だけ知りたい方は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++ を書きましょう。低レイヤー楽しいよ!!

コメントする

メールアドレスが公開されることはありません。