ま、そんなところで。

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

埋め込み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);

リファレンス

debugpyを使ったリモートデバッグ

直接実行できないpythonコードのデバッグに便利

インタプリタやデバッガで直接開始できないpythonコードをデバッグするには、 多くの場合リモートデバッグを使いますね.

pythonでは debugpy というモジュールを使います. github.com

組み込みpythonのdebugに使用できるので便利です.

Step1. 対象コードにdebugpyモジュールのコードを埋め込む

デバッグ対象のコードにデバッグサーバを立ち上げて、接続待機するコードを埋め込みます.

    import debugpy

    # port : 受付port
    port = 5678
    debugpy.listen(port)
    # addressとportを指定する場合はtupleでまとめて指定
    # debugpy.listen((address, port))

    # デバッガが接続されるまで待機
    debugpy.wait_for_client()

たったこれだけ.

Step.2 launch設定

Visual Studio Codeのlaunch設定に, pythonのリモートデバッグ設定を追加します.

// プラグインデバッグ用
{
    "name": "Python Plugin Debug (Attach)",
    "type": "python",
    "request": "attach",
    "connect": {
        "host": "localhost", // debugpy.listen している接続先ホスト
        "port": 5678           // debugpy.listenしているポート
    },
},

Step.3 接続とアタッチ

debugpy実行側のpythonコードを実行すると、wait_for_client のところで接続待機するので、デバッガを起動してアタッチします.

初回起動時などは、サーバ側が立ち上がるまで数秒かかることがあるので注意.
接続エラーが出る場合は、2〜3秒時間を開けて再度接続を試みると良いでしょう.

Windowsの場合は特殊な準備が必要

Windowsで使用する場合は、サーバ起動前にdebugpyにインタプリタ環境へのパスを設定してやらないと接続に失敗します.

以下にissueがありました.

github.com

こんな感じです.

# Only windows.
# debugpyにインタプリタパスを通知する
python_path = Path(sys.prefix).joinpath('python.exe')
debugpy.configure(python=str(python_path))

debugpy.listen(address_or_port)
debugpy.wait_for_client()

関数にしておくと便利

まとめて関数化しておくと便利です

def debug_wait_for_attach(listen_to):
    if os.name == 'nt':
        # for windows.
        python_path = Path(sys.prefix).joinpath('python.exe')
        debugpy.configure(python=str(python_path))
    debugpy.listen(listen_to)
    debugpy.wait_for_client()

リファレンス

C/C++アプリケーションへPythonを埋め込む

アプリケーションの一部をPythonで作る

pythonの公式Docには組み込み可能なpythonパッケージの記述があります.
これによると、Windows版にはアプリと一緒に配布できるpython環境があるそうです.

ドキュメントを見るととても容易にできるようなので、試してみました.

(準備) 通常のパッケージをインストール

組み込みpythonのパッケージには、ビルドに使用するヘッダやリンクライブラリ(.lib)が付属していません。 このため、通常パッケージを使ってビルドして、デプロイ時にembededdable版を配置するようにします.

以下からembeddable版と対応する通常版をDownloadしてインストールしておきます.

Python Releases for Windows | Python.org

テスト用最小コード

以下、テスト用の最小コードです.
いくつか注意点があります.

#define PY_SSIZE_T_CLEAN

// デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化.
#ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
#else
    #include <Python.h>
#endif

// 使用する実行環境のPythonインタプリタのパス
// ここで指定するインタプリタパスを利用してpythonモジュールの探索先など実行環境のパスが決まる.
// ここにvenvで作った仮想環境のpythonインタプリタパスを指定した場合、
// 仮想環境下でランタイムが動作する
#define PYTHON_EXE_PATH u8"Path to python"

int main(int argc, char** argv)
{
    int result = 0; 
    // このポインタはFinalizeが完了するまで保持する
    wchar_t* program = Py_DecodeLocale(PYTHON_EXE_PATH, NULL);
    if (program == nullptr) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
    }
    else {
 
        // 使用したい実行環境のpythonインタプリタのパスを指定
        Py_SetProgramName(program);

        // 初期化
        Py_Initialize();

        // Python実行環境の参照パスを出力してみる
        PyRun_SimpleString("import sys");
        PyRun_SimpleString("print(sys.path)\n");
        PyRun_SimpleString("print(sys.prefix)\n");
        PyRun_SimpleString("print(sys.exec_prefix)\n");

        constexpr int ERROR_RET      = 1;
        constexpr int SUCCESS_RET  = 0;

        // 環境終了処理
        int finalize_result = Py_FinalizeEx();

        int result = (finalize_result < 0) ? ERROR_RET : SUCCESS_RET;
        PyMem_RawFree(program);
    }
    retrurn result; 
}

(注意点1) embeddableにはDebug版が付属しない

Windowsの場合Python.h内に pragma があり、自動的にヘッダに対応する環境のpythonXX.libが参照されるようになっています.
ただし、マクロ _DEBUG が定義されているとデバッグ版 pythonXX_d.lib のほうがリンクされてしまいます.

embeddable python環境にはデバッグ版dllは付属していないので、 デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化します

// デバッグ版へのリンクを避けるため _DEBUG を一時的に無効化.
#ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
#else
    #include <Python.h>
#endif

(注意点2) Py_SetProgramNameにはインタプリタのパスを指定する

Python/C API Reference Manual — Python 3.10.5 documentation
Py_SetProgramNameの使い方について、公式Docをみると以下の説明がありました.

This function should be called before Py_Initialize() is called for the first time, if it is called at all. It tells the interpreter the value of the argv[0] argument to the main() function of the program (converted to wide characters). This is used by Py_GetPath() and some other functions below to find the Python run-time libraries relative to the interpreter executable.

インタプリタ実行ファイルからの相対パスからpythonのランタイムライブラリを見つける」とある一方で、 引数には「(C/C++のmain関数の引数)のargv[0] を入れる」との記載があり、前半と後半の文章の内容に食い違いがあります.

不思議に思って実際にpythonソースコードを追ってみたところ・・・
「argv[0] を入れろ」との記述は、 argv[0] に入るのがpythonインタプリタ実行ファイルパスであることを想定しているためのようです.

確かに、pythonファイル(.py)の実行だけを想定すれば、 argv[0]には常に python 実行ファイルへのパスが入ることになりますね.
しかし、pythonランタイムをC/C++アプリケーションに埋め込む場合は、この前提が当てはまりません.
公式Docの説明は、文章の後半は正確な内容ですが前半には表現に誤りを含んでいると理解するのが良さそうです.

ここは 使用する実行環境のインタプリタ実行ファイルのパス と読み替えて、
使用したい実行環境に配置されているインタプリタ実行ファイル(python.exe もしくは python)へのパスを指定します.

    // **使用したい実行環境のpythonインタプリタ実行ファイルのパス** を指定するっぽい
    //「プログラムファイルパス」は単純にmainの引数argv[0]を入力するというわけではなさそう.
    Py_SetProgramName(program);

実際、ProgramNameにインタプリタ絶対パスを指定してみると、組み込んだ環境のモジュール参照パス sys.path が、指定したインタプリタのある実行環境のものに変化することがわかります.
この仕組みによって、venvで作った仮想環境もC/C++に組み込むことが可能というわけですね.

結構使えそう

Initializeが完了すれば、あとはPythonの対話モードを起動した状態に似ています.
pythonの対話モードを起動した状態でC/C++コードを使ってコードを流し込んでいく感じです.

    // Python実行環境のモジュール参照パスを出力
    PyRun_SimpleString("import sys");
    PyRun_SimpleString("print(sys.path)\n");
    PyRun_SimpleString("print(sys.prefix)\n");
    PyRun_SimpleString("print(sys.exec_prefix)\n");

かなり手軽に利用できます.

リファレンス

家庭内LANでIPv6のグローバルIP(GUA)とLAN内専用の固定IP(ULA)の両方を使う

プライベートな固定IPv6アドレスがほしい

IPv6対応の家庭用ルータが普及してきた

市販の家庭用ルータを使う我が家のネットワークでは、LAN内はIPv4固定IPアドレスを各マシンに割り当ててsshしたりプライベートなサーバ立てたりして使ってたんですが、昨今では、プロバイダからIPv4のアドレスだけでなくIPv6プレフィックスが当然のように割り当てられるようになりました.

いよいよIPv6対応が当然の前提とされるようになると、アプリなどでIPv6アドレスでのテストなどもしなきゃダメなので、IPv6の固定アドレスなんて欲しくなります.

IPv6のリンクローカルアドレスはどうにも使いづらい

通常、プロバイダの接続サービスと家庭用ルータがIPv6に対応していて、マシン側のIPv6が有効になっていれば、プロバイダからグローバルネットワーク識別子の払い出しを受けたルータからのRA(Router Advertisement; ルータ広告)によって、ローカルネットワーク内の各マシンでグローバルスコープのGUA(グローバルユニキャストアドレス)が自動で構成されます.
それと同時にローカルネットワーク内専用のリンクローカルアドレス(fe80::/64)が各マシンで自動的に生成されて、固定IPのように使えることになってるのですが、リンクローカルアドレスを通信で使うには、IPv6アドレスに加えてScopeIDなんてのが必要だったりします.
このScopeID、困ったことにツールやアプリごとに指定する方法が異なっている上に場合によっては対応していなかったり、どうにもこうにも使いづらいんですよね.

あれ? 自動構成のGUAとULAを同時に持たせられない?

幸いIPv6は一つのインタフェースに対してアドレスを複数持つことが出来る仕組みなので、自動構成によって割り当てられるグローバルIP(GUA)を持たせつつIPv4のプライベートアドレスっぽく使えるULA(ユニークローカルアドレス)を各マシンに固定IPとして割り当てて使うことは可能なはずです.

しかしながら、LinuxのNetworkManagerやmacOSでは、ネットワーク設定などでIPv6アドレスを 手動設定 とすると、ルータからのRAによるGUAの自動構成が行われなくなるため、ローカルネットワークの外とIPv6での通信が行えなくなり、自動構成のGUAを使うのか手動割当のULAかいずれかを選択する、といった状況になってしまいます.
さすがにこれでは本末転倒です.

そこで、もうちょっとなんとかできないかな〜、と思い立った次第です.

あとから追加でULAを割り当てる方法なら意外に簡単

GUAの自動構成はとりあえずそのままにして、あとからコマンドを使ってULAを追加割当てする方法なら比較的に容易に実現できますね.
ここではLinuxmacOSの手順を示します.

(準備) ULAのサブネットプレフィックスを準備する

ULAはプライベートネットワーク内での使用が想定されているとはいえ、グローバルスコープのUnicastAddressとして定義されてるものですから、SubnetPrefix部分はちゃんと RFC4193 に従ってEUI-64識別子を生成しなければなりません.

ただ、このEUI-64識別子の生成は手計算では到底無理なのでツールを使います.

こちらに生成用のpythonコードが公開されてますので、これを使いました.
www.jdoodle.com

ネットワーク内の代表マシンを適当に一つ選び、そのマシンが使用するネットワークインタフェースのMACアドレスから識別子を生成します.

$ ./ula_generator.py
Input MAC address: 12:34:56:78:ab:cd  # ←ここにMACアドレスを指定する

ULA Prefix        -> fd98:c3f9:eb41::/48
First Subnet      -> fd98:c3f9:eb41::/64
Last Subnet       -> fd98:c3f9:eb41:ffff::/64
First IPv6 Address-> fd98:c3f9:eb41::1/64

ULA Prefixが得られたので、サブネット識別部を 1 (0001) とし、fd98:c3f9:eb41:1::/64 をサブネットプレフィックスとします.

(Step1) とりあえず手動で割り当てて試してみる

サブネットプレフィックスができたら、まずは手動でULAを追加割り当てしてみます.
とりあえずこれでGUAとULAとリンクローカルアドレスの3タイプのIPv6アドレスを持つ状態にすることが出来ます.

(Linuxの場合) ip コマンドを使う

Linux系OSは ipコマンド で割り当てます.

# (コマンド)
$ sudo  ip -6 addr add {IPv6Addr}/{PrefixLength} dev {IntarfeceName}

# (例: eno1 に fd98:c3f9:eb41:1::2/64 を割り当て)
$ sudo ip -6 addr add fd98:c3f9:eb41:1::2/64 dev eno1

コマンドを実行したら、 ifconfig コマンドでネットワークの設定を確認します.

$ ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.2  netmask 255.255.255.0  broadcast 192.168.1.255
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:db31  prefixlen 64  scopeid 0x0<global>
        inet6 fd98:c3f9:eb41:1::2  prefixlen 64  scopeid 0x0<global>  ★追加されている
        inet6 fe80::7404:eeb4:6b91:34b8  prefixlen 64  scopeid 0x20<link>
        inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:b06f  prefixlen 64  scopeid 0x0<global>
        ether 70:85:c2:7f:bb:7d  txqueuelen 1000  (イーサネット)
        RX packets 134402  bytes 68324237 (68.3 MB)
        RX errors 0  dropped 22346  overruns 0  frame 0
        TX packets 87596  bytes 20566693 (20.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 16  memory 0xa3200000-a3220000  

さらに、routeコマンドで静的ルーティングテーブルを確認します.

$ route -6
カーネルIPv6 経路テーブル
Destination                    Next Hop                   Flag Met Ref Use If
ip6-localhost/128              [::]                       U    256 2     0 lo
2405:xxxx:xxxx:xxxx::/64       [::]                       U    100 1     0 eno1
fd98:c3f9:eb41:1::/64          [::]                       U    256 3     0 eno1  ★追加されている
fe80::/64                      [::]                       U    100 2     0 eno1
[::]/0                         _gateway                   UG   20100 13     0 eno1
   :
(省略)
   :

(macOSの場合) ifconfig を使う

macOSでは ipコマンドは標準で組み込まれてないようです.
Homebrewを使って iproute2mac をインストールすれば ip コマンドが使えるのですが、 まずは標準の ifconfig で行うことにしました.

(コマンド)
$ sudo ifconfig {IntarfeceName} inet6 add {IPv6Addr} prefixlen {PrefixLength}

(例: en0 に fd98:c3f9:eb41:1::4/64 を割り当て)
$ sudo ifconfig en0 inet6 add fd98:c3f9:eb41:1::4 prefixlen 64

コマンド実行後、 ifconfig でインタフェースのアドレスを確認します.

$ ifconfig 
       :
    (省略)
       :
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=400<CHANNEL_IO>
    ether 88:e9:fe:4e:3d:19 
    inet6 fe80::8a6:a219:8c03:1cb6%en0 prefixlen 64 secured scopeid 0x4 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:27ae prefixlen 64 autoconf secured 
    inet6 2405:xxxx:xxxx:xxxx:yyyy:yyyy:yyyy:7a3d prefixlen 64 autoconf temporary 
    inet6 fd98:c3f9:eb41:1::4 prefixlen 64  ★追加されている
    inet 192.168.1.4 netmask 0xffffff00 broadcast 192.168.1.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active

さらに、 netstat -rn で静的ルーティングテーブルを確認.

$ netstat -rn
Internet6:
Destination              Gateway                         Flags           Netif Expire
default                  fe80::8a6:a219:8c03:1cb6%en0    UGcg              en0       
::1                      ::1                             UHL               lo0       
     :  
   (省略)
     :
fd98:c3f9:eb41:1::/64    link#4                          UC                en0     ★追加されている
fd98:c3f9:eb41:1::4      88:e9:fe:4e:3d:19               UHL               lo0     ★追加されている

(Step2) マシンの起動時にコマンドで自動付与するように構成する

コマンドで付加したULAはマシンをシャットダウンするたびに消えてしまうので、このままでは毎回起動するたびにログインしてコマンドで付与しなければいけません.
ならば、マシンの起動時に自動的にULAを付加できるようになれば本格的に運用できそうです.

(Linuxの場合) NetworkManagerのフックスクリプトを使う

デスクトップ版のLinuxでは、Network関係の設定は通常 NetworkManager が担っているようです.
NetworkManager には dispatcher の仕組みがあって、 /etc/NetworkManager/dispatcher.d/スクリプトや実行ファイルを配置すると、ネットワークデバイスの起動や状態変更のタイミングで呼び出してくれるようです.

以下のスクリプトを配置しました.
ファイルの所有者はrootにしておく必要があります.

#!/usr/bin/env bash

set -e

#
#  $1 = interface name. 
#  $2 = interface state. 
#

# $2をみて interface の 'up'と'down'のタイミングででコマンドを実行.

if [ "$2" = "connectivity-change" ]; then
    exit 0;
fi
 
if [ -z "$1" ]; then
    echo "$0: called with no interface" 1>&2
    exit 1;
fi

IP_CMD=$(which ip)

# インタフェース名
IFACE_NAME=eno1
# 追加で割当てるIPv6のULAとプレフィックス長
IP6_ULA=fd98:c3f9:eb41:1::2/64

if [ -n "${IP6_ULA}" ]; then 
    if [ "$1" = "${IFACE_NAME}" ]; then
        case "$2" in
            up)
                ${IP_CMD} -6 addr add ${IP6_ULA} dev $1 
                ;;
            down)
                ${IP_CMD} -6 addr del ${IP6_ULA} dev $1
                ;;
            *)
                # Do nothing
                ;;
        esac
    fi
fi

exit 0;

(macOSの場合) launchd を使う

macOSの場合、非ログイン状態でNetworkManagerのようにネットワークデバイス起動/切断時のフックを行う仕組みはちょっと見当たりませんでした.
なにか良い方法があればと思いますが、まずは設定だけでも・・・と、launchdを使って起動時にコマンドを実行させるようにしました.

ただし、コマンドの実行はNetworkデバイスのSetupが完了した後でなければ正しくルーティングテーブルが更新されません.
そこで、launchd のスクリプト起動タイミングには RunAtLoad ではなく WatchPaths を使い、マシンの起動後にネットワークサービスが DNS情報/etc/resolv.conf を変更したタイミングで実行させるようにします.
(※ 今回macOSでは起動後のip付加のみ. downイベント相当のフックに使えそうなものはちょっと見当たらず・・要検討...)

以下の設定ファイルを所有者をrootにして /Library/LaunchDaemons/ に配置します.
呼び出すスクリプトファイルは、 /Users/hoge/bin/reg_ipv6_ula.bash としました.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">

<!-- jp.or.regv6ula.plist. -->
<!-- /Library/LaunchDaemons/ に配置 -->

<plist version="1.0">
<dict>
    <key>Label</key>
    <string>reg_ipv6_ula</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/env</string>
        <string>bash</string>
        <string>/Users/hoge/bin/reg_ipv6_ula.bash</string>
        <string>add</string>
    </array>
    <key>Disabled</key>
    <false/>
    <key>WatchPaths</key>
    <array>
        <string>/etc/resolv.conf</string>
    </array>
    <key>KeepAlive</key>
    <false/>
    <key>StandardErrorPath</key>
    <string>/Users/hoge/log/reg_ula_err.log</string>
</dict>
</plist>

呼び出すスクリプトファイル(reg_ipv6_ula.bash)の内容は以下の通り.

#!/usr/bin/env bash

#
# 登録/削除用Script
#

IFCFG_CMD=$(which ifconfig)

# インタフェース名
IFACE_NAME=en0
# 割り当てるULA
IPV6_ULA=fd98:c3f9:eb41:1::4

if [ -n "$1" ]; then
    case "$1" in
    remove)
        echo "unregister ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 remove ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    add)
        echo "register ULA.. ${IPV6_ULA}"
        ${IFCFG_CMD} ${IFACE_NAME} inet6 add ${IPV6_ULA} prefixlen 64
        echo "Done."
        ;;
    *)
        ;;
    esac
fi

(参考) ところで、Windowsではどうするのだろう?

Windowsの場合はネットワークのプロパティからIPv6手動割当て とした場合でも、手動割り当てしたULAに加えてGUAもちゃんと自動構成されるようでした.
この動作だと「ULAだけ使う」という選択肢はなさそうですが、こちらは設定が簡単で良いですね.

参考サイト

pyenvでインストールしたpython環境でデバッグができなくて困った件

あれ?デバッガ起動できない・・?

pyenv、 複数のpythonバージョンをあれこれ切り替えることが出来て便利ですね.
システムにインストールされてない特定のバージョン環境で動作するスクリプトを書く必要があるときは、いつも使ってるおなじみツールです.
pyenv-virtualenv の仮想環境モジュールと組み合わせるとさらに利便性が高まりますね.

github.com

github.com

さて、例にもれず特定バージョン向けスクリプトを書くことがありまして・・・
そろそろ動作を見てみようかな、とpyenvでインストールしたpython環境を使ってデバッグをしようとしたところ、以下のエラーに遭遇.

/usr/bin/env /home/hoge/.pyenv/versions/3.8.6/bin/python /home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher 36261 -- /home/hoge/work/presnip/pyscr_dev/prj/main.py 
Traceback (most recent call last):
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/__main__.py", line 97, in <module>
    main()
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/__main__.py", line 24, in main
    from debugpy.launcher import debuggee
  File "/home/hoge/.vscode/extensions/ms-python.python-2021.12.1559732655/pythonFiles/lib/python/debugpy/launcher/../../debugpy/launcher/debuggee.py", line 8, in <module>
    import ctypes
  File "/home/hoge/.pyenv/versions/3.8.6/lib/python3.8/ctypes/__init__.py", line 7, in <module>
    from _ctypes import Union, Structure, Array
ModuleNotFoundError: No module named '_ctypes'

んー?
モジュールが無いぞ!・・・と怒られてデバッガが起動してくれません.

ctypeモジュールが見当たらない!?

ModuleNotFoundError: No module named '_ctypes'

ctypesモジュールが見つからないよ!とあるのですが、ctypesって標準でインストールされるはず・・・
んなアホな・・・と versions以下にあるC拡張モジュールのライブラリディレクトリ(3.8.6/lib/python3.8/lib-dynload) を見に行くと・・・

あれまー、ほんとに無い.

あるはずの _ctypes.cpython-38-x86_64-linux-gnu.so が見当たりません.
どうやらビルドされてないようです.

ビルド環境にlibffiが無いとctypesモジュールはビルドされない

いろいろ探し回ったところ、どうもこれっぽいなと思われる記事を発見.

pyenvのpython環境インストールは、インストール環境でpython環境を一式ビルドする方式なんですが、 ビルド環境でlibffiがみつからない場合は ctypeモジュールのビルドをSkipする んだそうで...

stackoverflow.com

いやこれ、python環境でも結構重要な位置づけのモジュールじゃなかったっけ!?
無しでもビルドできちゃうって・・これでいいんだろうか・・・?

$ sudo apt install libffi-dev

$ pyenv install 3.8.6
pyenv install 3.8.6
pyenv: /home/hoge/.pyenv/versions/3.8.6 already exists
continue with installation? (y/N) y

ということで、Ubuntu環境へ libffi の開発用パッケージ libffi-dev をインストールして、pyenvの環境を再インストール(再ビルド)したところ・・・

無事デバッガが動作するようになりました.

めでたく一件落着なんですが、ちょっとこれはメッセージから原因までパッっと分かりづらかった・・・
少なくとも、インストール時に警告ぐらい出してくれてもいいんでないかな・・・

参考情報

Red Hat Enterprise Linux の場合

RedHatの場合は、libffi-devel パッケージですね.
依存関係の都合上、他の開発パッケージ導入時に自動的に入ってるみたいです.

$ sudo yum install libffi-devel

macOSの場合

macOSの場合はbrewでinstallすれば良いみたいです.
こちらは libffi Fomula.
こちらも結構いろんなモジュールから依存関係があるみたいで、どうやら llvm 入れたときに一緒に入ったようでした.

$ brew install libffi

UbuntuのVSCodeでlldbを使ってRustコードをデバッグしたらブレークポイントが効かなくて困った

Rustコードのブレークポイントで止まらないんだけど・・・

UbuntuVSCodeをつかったRustの開発環境を作ってたんですが、lldbでデバッグしようとすると、どうもブレークポイントで止まらなくてハマりました.

location zero

ターミナルからlldbを直接たたいてブレーク設定しようとしても、どうもダメっぽいのですね.
ブレークポイントを追加しても位置表示が「Location: 0」となっていて、有効化できないのです.

lldbを直接起動して、listコマンドしてみてもコードが表示されず.
あー、これはどうやら完全にシンボルが見えてないご様子.

誰かが同じ問題にぶつかってやしないかな〜と、Google先生で検索を試みてもまったくそれらしい情報がないのです.
仕方ないので、諦めてしばらくの間デバッガはgdbをつかってました.

最近、思い立ってもう一度じっくり調べてみたところ、ようやく原因が判明.
lldbでデバッグできるようになるworkaroundもみつけることができたのでメモ.

環境

環境 : Ubuntu 20.04
VS-Code : 1.57.1
CodeLLDB : 1.6.5
Rust-Analyzer: v0.2.662

ソースコードをsymlinkを含むパスにおいていることが原因

どうやらソースコードがsymlinkを含むパスにあると、デバッグ時にlldbがシンボルを見つけられないようなのです.
よもやよもや、です.
実行は出来てるのに、同じファイル内にあるシンボルが見えてないとは・・・

似たような問題が3年ほど前のCodeLLDBのバグ報告で議論されてました.
mountしてる拡張ディスク上にプロジェクトを配置してた人がこの問題にぶつかっていたようです.

どうもこれ、CodeLLDBではなくlldbの問題なんだそうで.
確かに、lldb単体でも起きるのでCodeLLDBは関係ないっぽいですね.
いまでもOpen状態なところをみると、lldb側で対応してないのか根本的な解決には至ってないということでしょうね.

github.com

Workaroundみつけた!

スレッドの方にユーザ設定のsettings.jsonでlldb.launch.sourceMapを設定したら動くよ・・とあったので試してみましたが、どうもダメっぽいですね・・・

// settings.json
{
    // これはダメっぽい...
    "lldb.launch.sourceMap": {
        "/home/fuga/workspace/gitprjs": "/home/fuga/work"
    },
}

幸いこれをヒントにworkaroundをみつけることができました.
launch.jsonデバッグ設定にsourceMapエレメントを定義してlldbにパスの対応を教えてやれば、この問題を回避できるようです.

// launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lldb",
            "request": "launch",
            "name": "Debug lldb executable 'g_game'",
            "cargo": {
                "args": [
                    "build",
                    "--bin=g_game",
                    "--package=g_game"
                ],
                "filter": {
                    "name": "g_game",
                    "kind": "bin"
                }
            },
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopOnEntry": false,
            "sourceLanguages": ["rust"],
            // *** ここを追加 ***
            "sourceMap": {
                // 以下のsymlinkの場合.
                // work -> /home/fuga/workspace/gitprjs
                // real path -> symlink path の対応をそれぞれ絶対パスで定義してやる.
                "/home/fuga/workspace/gitprjs": "/home/fuga/work"
            }
        }
    ]
}

Linux環境で(Macもかな?)作業ディレクトリパスにsymlinkを使っていないと出くわすことのない問題なので、誰も気づかなかったのかもしれませんね.
ともかく、gdbとくらべて表示される情報量が多いlldbでデバッグできるようになったので、まずは良しとします.

UbuntuでPPTPクライアントをセットアップする

とあるサーバーに固定IPで接続する必要がありまして、VPN接続を設定したのでメモ.

今回はInterlinkのMyIPというサービスを使いました.
なかなか固定IPのサービスは高価なので、安価に固定IPを使用したいときは良いかもですね.

接続はWindowsMac, RedHatLinuxなら公式ページに接続方法が紹介されていますが、 DebianLinuxの設定例はありませんでした.

Ubuntu では NetworkManager からの設定でPPTPの設定ができないため、設定ファイルとスクリプトを駆使してPPTP接続を行う必要があります.
公式ページの手順(RHEL/CentOS系)とはずいぶん異なる感じなので注意.
(※ 16.04, 18.04, 20.04 の 各LTSバージョンで動作することを確認しました.)

1.PPTPの接続設定

Step1. PPTPクライアントをインストールする

まずはPPTPクライアントをインストールします.
これでpppなど依存パッケージもまとめてインストールされます.

$ sudo apt install pptp-linux

Step2. PPTP設定ファイルを作る

PPTP接続のための設定ファイルを配置します.
下記のコマンドを実行すると、設定ファイルの雛形を作って配置することができます.

$ sudo pptpsetup --create {接続名} --server {PPTPサーバのホスト名orIPアドレス} --username {ユーザ名} --password {パスワード} --encrypt

コマンド実行するとpeers以下に設定ファイルが作られ、chap-secrets が更新されます.

(1) peers設定ファイル

このファイルは内容だけを確認すればOKです.
特に修正する必要はありません.

/etc/ppp/peers/{接続名}

# written by pptpsetup
pty "pptp {サーバホスト名orIPアドレス} --nolaunchpppd"
lock
noauth
nobsdcomp
nodeflate
name {ユーザ名}
remotename {接続名}
ipparam {接続名}
require-mppe-128

(2) chap-secrets の編集

次に、/etc/ppp/chap-secrets を編集します.
ファイルに接続先とユーザ名/Passwordと割当IPの設定が追記されているので、こちらを編集します.

pptpsetup が生成した設定ではPPTP接続時の割当IPアドレスの部分が * になっていますが、 固定IPが割り当てられる場合は、ここに割り当てられているIPを入力します.

# Secrets for authentication using CHAP
# client server secret IP addresses
# added by pptpsetup for HogeHoge
{ユーザ名} {接続名} {パスワード} {PPTP接続時の割当IPアドレス}

Step3. ルーティングテーブルの書き換えスクリプトの準備

接続時に使用するルーティング変更処理を接続時スクリプト(ip-up, ip-down)として配置します.
内容は以下のような感じです.

(1) ip-up.local

/etc/ppp/ip-up.local (※なければ作る. filemode=0700)

#!/bin/sh
# 全ての接続をVPN経由にする場合はこちら
route add default gw {PPTP接続時の割当IPアドレス}

# 特定ホストのみをVPN経由でアクセスしたいような場合は下記の設定を追加.
# route add -net {VPN経由で接続するホスト名orIPアドレス} netmask 255.255.255.255 gw {PPTP接続時の割当IPアドレス}
exit 0

(2) ip-down.local

/etc/ppp/ip-down.local  (※なければ作る. filemode=0700)

#!/bin/sh
route del default gw {PPTP接続時の割当IPアドレス}

# 特定ホストのみVPN経由アクセスとしていた設定を削除
# route del -net {VPN経由で接続するホスト名orIPアドレス} netmask 255.255.255.255 gw {PPTP接続時の割当IPアドレス}
exit 0

Step4. 接続/切断スクリプトの準備

PPTPの接続と切断を行うスクリプトを作ります.
これはユーザが直接叩くので、作成・配置場所はお好みの場所でOKです.

(1) 接続スクリプト

ここではファイル名を startpptp.shとしました (※ filemode=0755)

#!/bin/sh
echo "Starging PPTP to {接続名}"
sudo pppd call {接続名} updetach
# デフォルトのルーティングを削除
sudo route del default gw {ルータのホスト名orIPアドレス}
echo "Connected!!"
exit 0

(2) 切断スクリプト

ここではファイル名を stoppptp.shとしました (※ filemode=0755)

#!/bin/sh
echo "Stopping PPTP to Interlink(XXXXXXXX)"
# pppdの停止
sudo pkill -TERM pppd
# デフォルトルーティングの復元
sudo route add default gw {ルータのIP}
# pppdによって追加されたPPTPサーバ接続のルーティングを削除
sudo route del -net {VPNサーバのホスト名orIPアドレス} netmask 255.255.255.255 gw {ルータのホスト名orIPアドレス}
echo "Disconnected."
exit 0

2.接続と切断

2-1. PPTPで接続する

$ ./startpptp.sh
f:id:zervoid:20200506090202p:plain

ifconfig コマンドで ネットワークアドレスを確認します.
ppp0が追加されていれば接続成功です.

f:id:zervoid:20200506085159p:plain

2-2. PPTPを切断する

$ ./stoppptp.sh

3. その他

起動時に自動的に接続するとかの場合は、NetworkManagerではなく、networkのinterface設定でpre-up/post-downなどを使うと行うと良いらしいです.

参考サイト