窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

Visual Studio for Mac Community 7.2 Preview (7.2.0.540)ではまった

私は Visual Studio for Mac を常に Alpha 版で運用しているのですが,当該バージョンでインストール及び起動に関してはまってしまったのでメモ.

ここにバグ報告してみた

まずは結論

アップデータで配布されていた最新バージョンの Mono (5.4.0.167) が問題(であると問題の切り分けから判断した)

起こったこととか

Visual Studio for Mac (以下VS4M) を使用中,アップデートがあることに気づき,アップデートダイアログから最新バージョンの Mono (5.4.0.167) と VS4M (7.2.0.540) をダウンロードを始める.

しかし双方ともダウンロードに失敗してしまったため,手動ダウンロードを試みる.

Mono

Mono は公式ページのアーカイブ を探すことでダウンロードが可能なため,ここからアップデータでダウンロード可能な Mono をダウンロードし,インストールを行った.

VS4M

一般的には公開されていない(?) URL のため,Xamarin のフォーラムなどを探し,Stable 版の直リンクを元に推測しダウンロード.Alpha 版などの Preview 版は,URL に Preview を含むということがわかった.

以上の作業によりアップデートダイアログ経由ではなく,手動でダウンロードしインストールが完了したため起動を試みたが,動作しなかった. ランタイムエラーなどかなと思ったが,詳細なログなどの見方がわからず頭を悩ませた.

検証

1.. 最新の Mono 5.4.0.167 + Alpha Preview な VS4M 7.2.0.540

アップデートダイアログで推奨されたバージョン同士の組み合わせ

2.. 最新の Mono 5.4.0.167 + Stable な VS4M 7.1.0.1297

最新の VS4M が起動できない何かしらのバグを含んでいるのかと考えたため,Stable 版の VS4M をインストール

3.. Stable な VS4M と同時にダウンロードされる Mono 5.2.0.215 + Alpha Preview な VS4M 7.2.0.540

Mono ランタイムのインタフェースなどが変わっていなければ動きそうだと考えたため,確実に動作する Mono を使用(リリースノートなどで確認するべきだった)

追記(8/31)

4.. VS4M 7.2.0.540 の前のバージョンで採用されていた Mono 5.4.0.135 + VS4M 7.2.0.540

5.. Nightly version の Mono 5.7.0.181 + VS4M 7.2.0.540

以上の3パターンで検証した結果,動作したのは 3 と 4 のみだった.

また起動のみではなく,現在開発しているプロジェクトのビルドも問題なく行えたため,何かしらの問題が起こるまでこの組み合わせで使用しようと思う.

追記

Mono 5.4.0.167 + VS4M 7.2.0.540 でも 日本語入力に関するIME を使わなければ起動可能であることがわかった. 要するに「ことえり」や「Google 日本語入力」を使わず,入力ソースから 英語 -> ABC を選んで,それを使うようにすれば良い.

追記の追記

戦犯はGoogle 日本語入力だったので,日本語入力を使わず標準IMEだけを使えば日本語でも問題ない. どうもサードパーティーIMEを使っているとレイアウトが取得できないっぽい.

やるべきこと

  • アップデート前に現在のバージョンを調べておく

今回使用していた Mono のバージョンを保存せずアップデートを行ってしまったので,元のバージョンに戻せなくて厳しかったため

  • ログの見方を調べる(解決)

${HOME}/Library/Logs/VisualStudio/7.0 に詳細な起動ログとかが載ってた

最後に

アップデートダイアログに出てるリリースノートに飛べないのどういうことなんですかねぇ… f:id:yamachu_co:20170827104952p:plain

(追記8/31) 8/28 日にアクセスできるようになってた.

Alpha 版の醍醐味はこの辺にあると思った

Pythonのctypesを使ってネイティブライブラリを使う

また備忘録として.

前回は NuGet でプラットフォームごとにネイティブライブラリを同梱してそれを使うラッパーのパッケージを作る方法 - 窓を作っては壊していた人のブログ という感じでC# でネイティブライブラリをラップして使うようなことを紹介しました. 私はC# が好きだし,Xamarin Workbooks などがあるため REPL があるため C# だけでもいいかなと思っていたのですが,Python もあるとやっぱり楽だなぁということを思うようになりました.

ということで Python でラップするとどうなるの?ということをメモしていきたいと思います.

おことわり

今回は 16.16. ctypes — Pythonのための外部関数ライブラリ — Python 3.6.1 ドキュメント を使いましたが,今ネイティブライブラリをラップしたり速度を求めるなら一般的には Cython: C-Extensions for Python を使います. 今回 ctypes を使った理由としては,Cython を入れることが困難な環境(そんな環境あるのかよという感じですが… Windows ユーザーでも今は簡単に入りますよね?たぶん)とかでも簡単に使えるからと言う理由です. 自分の研究室のメンバーがあまり Python に詳しくなさそう()というのもあるので,とりあえずどんな制限下でも使えないといけないという…

ネイティブライブラリの準備

もはやここは gcc の使い方とか,Windows で言ったら cl.exelink.exe の使い方の話になるので省略.

github.com

このリポジトリmakefilebuild_windows.bat を見れば大体わかります. Windows の nmake が使いたかったのですが,あまり理解できていないので cl.exe と link.exe を愚直に叩くやり方で頑張っています.

今回は以下のようなライブラリを想定してやってみます.

void mod_1d_array(double *arr, int size)
{
    for (int i = 0; i < size; i++) arr[i] *= 2;
}

int sum_1_to_value(int value)
{
    return value + 1;
}

void value2times(int *value)
{
    *value *= 2;
}

ネイティブライブラリを使う

とりあえず流れを追ってみましょう.

ライブラリをロードする

import ctypes

my_clibrary = ctypes.cdll.LoadLibrary(YOUR_C_DYNAMIC_LIBRARY_PATH)

my_clibrary に先程(?)用意したライブラリをロードしたものを格納します(語弊があるけど気にしないでください).

この my_clibrary の attribute にロードしたライブラリのシンボルが含まれているので,以下のようにして対象の関数を呼び出すことが出来ます.

関数を呼び出してみる

result = my_clibrary.sum_1_to_value(1)

この result には 2 が格納されていると思います.

非常に簡単,Python から C のライブラリが呼び出せるとか最高!という感じですが,まだまだ序の口,簡単な型だから出来たわけです.

それでは参照渡しとかそういうのをやってみましょう…と言いたいところですが,ちょっと待ってください. 引数の数だったり,引数の型,戻り値の型とかをちゃんとアノテーションしておかないと,後から見た時にわかりづらくないですか?

実際 ctypes を使う時は引数を誤ってしまった場合など,セグメンテーションフォルトで落ちたりします,つらいですよね. しかし予め型の定義をしておくと渡したパラメータがその型と互換性があるか,キャスト可能かなどの判定を行った後に呼び出されるので非常に安心です.

ということで,型の定義をしてみましょう.

型定義で安心して使えるようにする

今回用意した3つの関数について定義してみようと思います.

# void mod_1d_array(double *arr, int size) の場合
my_clibrary.mod_1d_array.restype = None
my_clibrary.mod_1d_array.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int]

# int sum_1_to_value(int value)
my_clibrary.sum_1_to_value.restype = ctypes.c_int
my_clibrary.sum_1_to_value.argtypes = [ctypes.c_int]

# void value2times(int *value)
my_clibrary.value2times.restype = None
my_clibrary.value2times.argtypes = [ctypes.POINTER(ctypes.c_int)]

こんな感じになります. ポイントとしては,配列やポインタなどの参照渡しの場合は,対象の型を POINTER で囲ってあげればポインタであるということが明示することが出来ます.

これだけでぐっと ctypes を使った時のネイティブライブラリへのアクセスが安全なものになります. Jupyter Notebook とかで開発している時はものすごい助けられました.

また副産物としては,このように attribute にアクセスするので,もしもそんな関数がシンボルとして定義されていない場合は例外を吐きます. ネイティブライブラリを実装している時にシンボルの公開忘れなどがあった場合そこで気づくことも出来ます(今回自分で実装していたライブラリで Windows でのシンボル公開を忘れているものがあったのにこれで気づくことが出来た).

それでは今回定義したもので安全に関数を呼び出してみましょう.

参照を伴う関数の呼び出し

まずはシンプルな変数の参照渡しをやってみましょう.

value = ctypes.c_int(10)
my_clibrary.value2times(ctypes.byref(value))
print(value.value)

20が出力されたでしょうか. このように値の参照渡しは byref によって行います.

さて,次は配列です. これはすこし厄介です. 上記の value2times は配列とかでもなんでもないものを渡すだけだったので byref だけで良かったのですが,今回は配列の確保なども行わなければなりません.

今回は 1 ~ 10 の値が入った配列を mod_1d_array で操作してみます.

my_array = list(range(1, 10 + 1)) # Python2.x ならこれでいいけど,3.x だと list(range(1, 10 + 1)) とかにしないとだったりする?
my_c_array = (ctypes.c_double * len(my_array))(*my_array)
my_clibrary.mod_1d_array(my_c_array, len(my_array))
print(list(my_c_array))

2.0, 4.0, …. みたいになったと思います.

Cライブラリに配列を渡す時は 目的の型 * 配列の長さ という を作って,そのインスタンスを作ります. ここでは ctypes.c_double * len(my_array) としていて,この場合 c_double_Array_10 という型になります.

そのためここでは my_c_array という c_double_Array_10 という型のインスタンス*my_array のようにリストを展開して初期化しています. ポインタ型と 1d-Array は型の互換があるので,そのまま渡しても自動的にキャストされてそのまま使うことが出来ています.

こんな感じでやり方さえ覚えてしまえば配列だろうが簡単に渡すことが出来ます.

早いけどおわりに

今回は ctypes を使ってネイティブライブラリを使う初歩をお伝えしました.

実際の環境ではこんな簡単なライブラリじゃなくてもっと型定義がややこしいものなどたくさんあると思います. そんな時は公式ドキュメントをちゃんと読んでみるとヒントがあったりします.

また構造体や2次元配列みたいのは少しテクニックが必要だったりします.

それについては実際に私がラップした

github.com

とかを見ていただければと思います.

毎回途中まで記事書いて力尽きるのやめたい

没ネタ

頑張って書こうとしたけど,まとめるのが面倒でソース見てって思ったもの

  • 多次元配列に関してはサポートモジュールを作っておくと楽
inner_type = (c_double * inner_length)()
outer_type = (inner_type * outer_length)()
buffer = outer_type()

for i in range(outer_length):
    buffer[i] = inner_type()

とか二次元配列用意する時に書くのアホらしすぎる.

またこれは c_double_outer_length_array_inner_length_array みたいな型定義になって, POINTER(POINTER(c_double)) の型とマッチしなくて,型チェックで弾かれてしまう.

だからこれを cast という形で整えてあげる必要がある. これも一気にやるサポートモジュールを作ると楽.

  • ライブラリ名とかを列挙して LoadLibrary し易い形に

__init__.py でやったこと. ctypes ではないけれども,pkg_resources を使うことによって,そのライブラリがあるパスを特定出来るので,安全にファイルを探すことが出来る.

  • 配布のための setup.py で pre-build を含ませる

Python の C拡張 を今回は使ったわけではないので,同梱したりするのが難しかった. これも ctypes とは関係ないけれども,setup.py で引っ張ってくるようにしたらある程度楽になると思う.