ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

埋め込みpythonでのみモジュールのModuleLoadErrorが起こる問題

埋め込みpythonでのみモジュールがModuleLoadErrorを起こす!?

C++アプリケーションへpythonコードを埋め込んで使用していると、pythonコード内でimportしているモジュールの中に、ModuleLoadErrorを起こして正しく動作しなくなるものがあります.
debugpyも例外ではなく、エラーが出てC++アプリへ組み込んだ状態でのpythonコードのデバッグができません・・・

しかし、問題を起こすpythonコードをpythonインタプリタから単独で実行したときは何の問題もなく動作するんですよね.
これはハマりました.

pythonモジュール側のシンボルが解決できないのが原因

どうも _ctypes が存在しないとかなので、pythonランタイム側のシンボルが見えていないようですが・・・

原因の可能性の一つは、libffi問題.
zv-louis.hatenablog.com
こちらは過去の記事にて解決済み.
事実、python インタプリタで実行した場合は動作しています.
明らかに別の問題です・・・

色々探してると、この問題に関する記事がありました.

stackoverflow.com

C/C++の拡張モジュールが、pythonモジュール pythonXX.so 内のシンボルを参照している場合に、実行時にシンボル参照を解決できないと発生する問題、とのこと.

いわれてみれば、現象が発生するモジュールはすべてC/C++拡張タイプのモジュールであるという特徴があります.
これは盲点でした.

確かに、C/C++拡張モジュールを作るとき、pythonXX.so を参照させるようにする必要がありました.
当然、実行時のシンボル解決には pythonXX.so のシンボルが見えていなくてはなりません.
ところが、拡張モジュールはどこにデプロイされてどこからロードされるかが不定なライブラリですから、事前にシンボル解決で参照する依存先をパスで指定することなどはできず、シンボル解決はもっぱら 参照可能なロード済みシンボル に依存せざるを得ないのでした.

組み込み先のシンボルを可視化する

--export-dynamicを指定してリンクする

pythonコードを組み込むモジュールのリンク時にリンカオプション -export-dynamic 指定をする.
これは、ロード済みシンボルを後でロードするモジュールのシンボル解決に使用可能にするオプション.

この件については、以下にリンカオプション例が示されています.
ちゃんと読まなきゃダメですね.

docs.python.org

python組み込み先が共有ライブラリの場合の注意点

pythonコードを組み込んだモジュールが共有ライブラリの場合、別の問題があります.

組み込んだ共有ライブラリをロードするときにdlopenを使う場合、--export-dynamic のリンク指定は無視されて、dlopenのロードオプションが適用されます.
ほとんどの実行環境では dlopen の RTLD_LAZY, RTLD_LOCAL いずれかがデフォルトになっているために、pythonモジュールがロードされるタイミングで必要なシンボルがすでにロードされて参照可能となっておらず、モジュールで必要なシンボルを解決できないことがあるので注意が必要です.

確実にロード済みシンボルを参照できるようにするには、 dlopenを使用するときに RTLD_NOWRTLD_GLOBAL オプションを明示的に指定する必要があります.

    // hoge.soへpythonXX.soをリンクして組み込んでいる場合
    void* hmod = nullptr;
    std::string mod_path("hoge.so");
    hmod = dlopen(mod_path.c_str(), RTLD_NOW | RTLD_GLOBAL);

リファレンス