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

提督の窓や成績の窓を作ってました.今では適当に好き勝手に開発をしてます.

SECCON Beginners 2017 仙台に参加してきた(ネタバレ含)

2017.seccon.jp

3つの講義を受けて後の演習で実際に難問か解くというもの. ビギナーの私にも理解できるような非常に丁寧で面白い講義でした.

以下覚えてることを適当に解き方と一緒に書く.

続きを読む

日本語を含むC++のプロジェクトをAppVeyorでビルドしたらはまった

Raspberry Piに喋らせてみるみたいなことで使ってる人もいるのかもしれない OpenJTalk. これをC# でラップして,ライブラリ化して使いたいなと思ったためちょこちょこと作業をしていました.

実際のC/C++のプロジェクトがこちら

GitHub - yamachu/LibOpenJTalk: Unofficial OpenJTalk mirror

そして成果物のC# ラッパーがこちら

github.com

以前このブログでも紹介したように各プラットフォームでネイティブのライブラリをビルドし,DllImport属性などを使ってラップしていくやり方で行っていたのですが,どうもWindows環境で日本語をネイティブ側に渡そうとすると文字コードの問題が発生してしまうと言った問題が起こりました.

結論としてはAppVeyorの言語ロケールが英語になっていたため,ソースコードUTF-8で保存されたものと解釈しそのままビルドが行われたりしたことが問題でした. またコマンドラインオプションを使用しないとビルドすら通らなかったため,AppVeyorでビルドする際はそのあたりも考えたほうが良かったみたいです.

まずはビルドを通るようにするには,cl.exe のオプションに対象ソースコード文字コードを記述します. 自分はSJISで保存されたファイルだったので /source-charset:.932 を追記しました.

こちらの記事が非常に参考になりました.

Visual C++(cl.exe)で UTF-8 のファイルをコンパイルする - Bite Code

この修正によりビルドが通るようになりましたが,複数のwarningが発生し,また生成されたライブラリで日本語の扱いがうまくいかないバグが発生しました. 実際の warning は以下のとおりです.

c:\projects\libopenjtalk\text2mecab\text2mecab_rule_shift_jis.h(75): warning C4566: character represented by universal-character-name '\u3000' cannot be represented in the current code page (1252)

ここの 1252 は Windows-1252 - Wikipedia のことを指します.

どうもロケールを変えてcode pageを今回ではSJISの932に変えてあげる必要がありそうです.

やり方は流石にAppVeyorとかのドキュメントに書いてあるだろうと思ったのですが,実際載ってなくてIssueを漁っていたら発見できました.

Support a different code page · Issue #846 · appveyor/ci · GitHub

日本語環境では

init:
- ps: Set-WinSystemLocale ja-JP
- ps: Start-Sleep -s 5
- ps: Restart-Computer

こんな感じです. ちなみにこのStart-Sleepを抜くと処理が返ってきません(このCIを回して他の作業をしていたのですが通知がこなくて見てみたら止まっていましたw)

こんな感じで日本語を含むプロジェクトでもAppVeyorでいい感じにビルドできそうです.

このミスに気づくのに数日かかったの本当に辛いお気持ちです…

私のネタ帳 - 随時更新 -

2017/09/04

  • dotnet code-generator の Visual Studio for Mac 向けのラッパーアドイン
    • (9/5) 雛形は完成したけど,gtkでダイアログ作らないとなのでMono developインストールしないと…
  • csx を使った Realm の初期データ挿入のマイグレーション支援アプリケーション

2017/09/06

最新の Mono のビルドをしている.

git clone --recursive --depth 1 https://github.com/mono/mono.git --branch master --single-branch で最新コミットとサブモジュールを全部引っ張ってきて,./script/mac-sdk-なんちゃらを叩く.

実際 Mono だけのビルドであれば他のビルド方法がある(Qiitaの私の記事参照)

ビルド中にllvmのビルドが始めるのだけれど,Valgrindのヘッダが見つからずビルドに失敗するので,llvmのビルドが始まる前にstage/includeにvalgrindのヘッダをコピーしておく. このディレクトリは実行権限ぐらいしかなくて,書き込みができないので,sudoで無理やり入れてからchownで戻すのがいいだろう.

追記

fsharpくんのビルドで落ちるからビルド対象から除いた.

更に追記(9月19日)

どうも自分のMacの環境が悪いんじゃないか疑惑. まっさらな状態にして再度ビルドをかけたらすんなりいった. そこで使ったビルドの手順は

Monoのビルドの時のコマンドメモ · GitHub

諦めが悪いので再度PRを出した

[gtk] Fix crashing when using Third-party IME by yamachu · Pull Request #49 · mono/bockbuild · GitHub

ASP.NET Core と Realm Object Serverを使ってクライアントとマスターデータの共通化を試してみたい

雑記とメモです.

現在作っている東北大学大学祭アプリのデータベースにRealmを使用しています.

昨年はサーバーはPythonDjangoで作成して,データベースはMySQLを使用していました. しかしクライアント側はRealmを使用していて,同期の難しさが問題となっていました.

そこで今回はサーバーサイドもRealmにしてしまいました. いいタイミングで.NET Core対応も来ていたところでもあるので.

blog.realm.io

そこで詰まったことと気づきをちょこっと書いていきます.

気づきとかつまったところ

  • 他のユーザーとデータベースを共有する設定のやり方がわからない

realm://{Realm Object Serverのアドレス}:{ポート}/__admin を開いて Permission のところで権限を与えたいユーザを追加する. 自分はこの方法でやったけれども,絶対に別の正しいやり方があるはず…

  • mayRead を与えた DB に書き込めない

ローカルのコピーになら書き込めるんでしょとか適当に考えていたらダメだった. 今回はマスターデータに追加のフィールドとしてお気に入りとかの boolean を追加したいと思っていたのだけれども,ダメそうなのでローカルに新たにそれ専用のフィールドを持った DB を作ってそちらに書き込む.

  • masterデータを管理するユーザに関して

今回は運営の人にデータを入れてもらうので,Admin以外で運営専用のアカウントを作成.間違って他のテーブルを破壊されても困りますしね.

  • Dockerで EXIT 99 になって動かない

未解決,どうしよう.とりあえずログ見てみよう.

  • Credentials.UsernamePassword(ユーザー名, パスワード, false)にしてるのに,create が常に True になる

こればっかりは本当に原因がわからなかった.dotnet clean とか rebuild とか試してたらいつの間にか出来るようになってた.

  • (追記)プロジェクトのある型だけをSchemeとして使用したい

JavaとかSwiftのやつでは見覚えがあって,dotnet ではどうするんだろうと悩んでいたけれども,頑張ってAPIドキュメントを探すと見つかる.

Class RealmConfigurationBase | Realm

と思ったらちゃんと書いてある…

https://realm.io/docs/xamarin/latest/#class-subsets

  • (追記 8/30) dotnet コマンドでビルドが出来ない

公式ドキュメントに

The dotnet command line tool is not supported when building for .NET Core. Please use Visual Studio or the msbuild command line tool to build your project.

こうあるように,Visual Studio を使うか,MSBuild コマンドでビルドすればいい. で,不親切なことに近くに MSBuild での注意点が書いてない…

ビルド方法は CI でビルドする時,みたいな Q&A を参考にすれば良い

VSCode とかでビルドタスクを書いてもいいし,コマンドラインから以下の様にすれば Fody のタスクが走って,いい感じに Realm が使える.

msbuild /p:SolutionDir=$(pwd)

(追記 9/5)

実際毎回 SolutionDir のパラメータを付けるのは面倒なので,ちょっとした対策をしてみた.

ソリューションファイル(.sln) があるのであれば,そのディレクトリとの相対パス

<PropertyGroup>
    <SolutionDir>..\</SolutionDir>
  </PropertyGroup>

こんな感じで csproj に追加してあげる.これで msbuild 一発で通るようになる.


また何か辛くなったら追記しよう.

また学祭アプリはサーバーサイドもクライアントサイドもいい勉強材料だったり,イベントに使用できるテンプレートでもあるので,公開準備ができ次第公開します.

(追記 8/30) ちょっとした雛形は github.com で公開しています.

使用している技術としては

  • ASP.NET Core MVC + Razor Pages
    • 独自のトークン認証機構のMiddlewareとかその辺も盛り込んでます
  • Realm
  • Xamarin.Forms
  • Docker
  • MySQL(Realmで管理しないユーザーの登録とかアンケートの集計関係)
  • Redis(発行したセッショントークンの管理)

デプロイ先はAzure VMを予定.

十分話せる内容ですね,勉強会で登壇する内容が増えて嬉しい(?

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 で引っ張ってくるようにしたらある程度楽になると思う.

NuGet でプラットフォームごとにネイティブライブラリを同梱してそれを使うラッパーのパッケージを作る方法

非常に自分で挑戦して悩んだことだったので,備忘録として残しておくことにします.

現在大学院で音声研究をしていて,音声信号処理系のライブラリを使うことが多いのですが,その多くが C/C++ で書かれていたりします. 実験レベルではシェルからパイプで繋いだり,スクリプトを書くというような流れていいと思いますが,実際に実験や研究から得られた知見を元にものを作ろうとした場合,C/C++ でなるべく書きたくありません,難しいですし. そのため C/C++ のライブラリを C# でラップして使うことが多く,その中で NuGet のパッケージにすると更に使いまわせることに気づき,このタイトルにあるようなことをやってみました.

はじめにおことわりしておきますが,Githubリポジトリを見てね案件が多いです,手を抜いてしまい申しわけないです.

NuGet のパッケージを作るには

ひとまず今回作ったライブラリをご紹介します.

github.com

World という音声分析合成ツールをラップしたものになります.

ものを見て理解する,という方は DotnetWorldlibrary/nugetディレクトリ内を見ていただければ理解できるかと思います.

さて,タイトルを満たす NuGet のパッケージを作る上で必要になるポイントは以下の4点です.

  • プラットフォーム毎のマネージドなライブラリ(ラッパー)を用意する
  • プラットフォーム毎のネイティブライブラリ(ラップ対象)を用意する
  • nuspec ファイルを用意する(プラットフォーム毎に出し分ける設定とかも)
  • (モバイルも対象にするのであれば)targets ファイルを用意する

基本的にはこの4点を頑張ればネイティブライブラリを含む NuGet パッケージは作れます.

プラットフォーム毎のマネージドなライブラリ(ラッパー)を用意する

ここを本来は頑張って書くべきではあるのですが,実際自分もまだマーシャリングなどを詳しく理解できているわけではないので,実際に今回扱った例を挙げて簡単に紹介いたします.

今回は PInvoke を多用しました.以前は C++/CLI でラップしていたのですが,Windows 以外での環境を想定した場合動作しないという問題があったからです.

PInvoke ってどんなもの?という感じですが,Win32API などを使ったことのある人にとっては見覚えのある DllImport 属性のものとイメージしていただければいいと思います.

例としては

if __Linux
        private const string DllName = "libworld.so";
#elif __Win
        private const string DllName = "world.dll";
#endif
        [DllImport(DllName,CallingConvention = CallingConvention.Cdecl)]
        public static extern void CheapTrick([In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] double[] x,
            int x_length, int fs, [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] double[] temporal_positions,
            [In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex=5)] double[] f0, int f0_length, [In] CheapTrickOption option,
            [In][Out] IntPtr[] spectrogram);

こんな感じです. ラップ対象のシンボル名のメソッドを用意して,引数に対して色々とアノテーションしてあげます.

今回の関数は C/C++ では

void CheapTrick(const double *x, int x_length, 
    int fs, const double *temporal_positions, const double *f0, 
    int f0_length, const CheapTrickOption *option, double **spectrogram)

として定義されています.

見ての通り基本的な型に関しては特に何か特別なアノテーションは必要ないのですが,配列や多次元配列,構造体に関してはアノテーションが必要になってきます.

配列に関して見てみると,[In][MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] double[] x という引数の定義になっています. これは [In] 属性がついているため,ネイティブ側での変更はなく一方向のメモリのマーシャリングのみでよいということがわかります.また [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] という属性では SizeParamIndex=1 とあるようにこの配列の大きさは 0 始点で,順番的に 1 の引数に与えられた数字の大きさであることを示しています. こうすることでマネージド環境での配列を非マネージド環境にその与えられたサイズだけ転送することが出来ます.

次に多次元配列ですが,こいつが非常に厄介です.正直なところ自分のやり方であっているのか少し自信がないところもあります. 実際探してみたのですが,今回のように double** として渡す方法がなさそうだったので,IntPtr の配列にして渡してしまいます(三次元以上になったらどうなるんだろう…).

以下,extern した関数を呼んでいるマネージドなコードです.

public static void CheapTrick(double[] x, int x_length, int fs, double[] temporal_positions,
            double[] f0, int f0_length, CheapTrickOption option,
            double[,] spectrogram)
{
    int outer = spectrogram.GetLength(0);
    int inner = spectrogram.GetLength(1);

    IntPtr[] ptrs_sp = new IntPtr[outer];

    for (var i = 0; i < outer; i++)
    {
        ptrs_sp[i] = Marshal.AllocHGlobal(inner * Marshal.SizeOf<double>());
    }

    CoreDefinitions.CheapTrick(x, x_length, fs, temporal_positions, f0, f0_length, option, ptrs_sp);

    var tmp_arr = new double[inner];
    
    for (var i = 0; i < spectrogram.GetLength(0); i++) {
        Marshal.Copy(ptrs_sp[i], tmp_arr, 0, inner);
        Buffer.BlockCopy(tmp_arr, 0, spectrogram, i * inner * sizeof(double), inner * sizeof(double));
        Marshal.FreeHGlobal(ptrs_sp[i]);
    }
}

やっていることとしては,二次元配列の大きさを調べて,IntPtr に対象の配列の型の大きさと配列の大きさでメモリを確保して非マネージド側に渡しているだけです. 上記のように予めどれだけのメモリが必要なのかを知っておく必要があるため,いい感じに設計する必要があります.(自信がないからこのあたりで切り上げる)

最後に構造体です.構造体でも簡単出来るものと出来ないものがあるのですが,今回は簡単なものについて触れたいと思います. 上記の CheapTrickOption はマネージド側ではクラスで定義されていますが,C/C++ 側では構造体で定義されています.

C# でのクラス定義は

[StructLayout(LayoutKind.Sequential)]
public class CheapTrickOption
{
    public double q1;
    public double f0_floor;
    public int fft_size;
}

C/C++ では

typedef struct {
  double q1;
  double f0_floor;
  int fft_size;
} CheapTrickOption;

となっています. C# 側で[StructLayout(LayoutKind.Sequential)]という属性を付け,型を同じにして順番も揃えてあげることで簡単に構造体をやり取りすることが出来ます. 面倒な構造体は中にポインタが入ったり,よく中身がわからない構造体が含まれているものなのですが,今回はなるべく触らないように逃げた& C/C++ 側の定義をちょっと変更したので述べません. 誰かわかったら教えてください.

と,以上の3つの要素を覚えればある程度のネイティブライブラリはラップできるかと思います. 更に踏み込んだライブラリは自分もラップしたことがないのでこの辺で〆たいと思います.

プラットフォーム毎のネイティブライブラリ(ラップ対象)を用意する

上記のステップでラッパーライブラリが完成したとします. あとは各プラットフォーム毎に共有ライブラリを作っていくだけです. 今回は Makefile が用意されていて単純なフローで作ることの出来るライブラリだったために,コンパイラとオプションをいじるだけで済みました.

以下メモ

  • Windows: cl.exe に /c オプションをつけてオブジェクトを生成し,link.exe に /dll オプションを付けて DLL を作る
  • Mac: -dynamiclib オプションを付けてオブジェクトをリンクする
  • Linux: -shared オプションを付けてオブジェクトをリンクする
  • Android: standalone toolchain をどうにかして頑張る.32bit の arm はオプションが若干変化
  • iOS: xcrun でコンパイラ関係探して流れに沿って行う.最後に lipo でまとめる.

上記の詳しい内容については

GitHub - yamachu/World: A high-quality speech analysis, manipulation and synthesis system

.travis.ymlappveyor.ymlMakefile などにまとまっています.

以上の流れに沿って各プラットフォームで共有ライブラリを作成します(iOSに限っては静的ライブラリ)

nuspec ファイルを用意する(プラットフォーム毎に出し分ける設定とかも)

Microsoft MVP である bonprosoft 氏の投稿の

blog.bonprosoft.com

を参考にしました. その為本記事ではこの内容に関しては割愛します.

またソースとしては DotnetWorld.nuspec を参照.

(モバイルも対象にするのであれば)targets ファイルを用意する

ここが一番意味苦労した部分だと思います. ターゲットがデスクトップ環境であれば runtimes/~/native 以下に置いておけば読んでくれたのですが,モバイル環境は別の操作が必要です.

非常に参考になったのは

realm-dotnet/Realm.nuspec at master · realm/realm-dotnet · GitHub

realm-dotnet/Realm.Database.targets at master · realm/realm-dotnet · GitHub

です(これがなかったら多分解決してなかった).

nuspec の方は特にこれと言ったものはないのですが,途中に targets ファイルを build フォルダに入れている流れが見えると思います. ここが非常に重要で,この targets ファイルは何をしているかというとビルドプロセスの中に入って何かしら決められたタスクをこなしています.

詳しい内容は

www.kekyo.net

が非常に参考になります.

この targets ファイルでビルド時にネイティブライブラリの位置を教えたり,属性はどんなものかなどを指定しています.

iOS を例に取ると

<?xml version="1.0"  encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <_WorldRootPath Condition="'$(_WorldRootPath)' == ''">$(MSBuildThisFileDirectory)..\..\native\</_WorldRootPath>
  </PropertyGroup>
  <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == 'Xamarin.iOS'">
    <NativeReference Include="$(_WorldRootPath)ios\universal\libworld.a">
      <Kind>Static</Kind>
      <SmartLink>True</SmartLink>
      <IsCxx>True</IsCxx>
      <LinkerFlags>-lstdc++</LinkerFlags>
    </NativeReference>
  </ItemGroup>
</Project>

ツリーにするとこんな感じに NuGet のパッケージは構成されていて(だいぶ省略しましたが)

├─build
│  └─Xamarin.iOS10
│          DotnetWorld.targets
│
└─native
    └─ios
       └─universal
          libworld.a

この Xamarin.iOS10/DotnetWorld.targets を始点にどこに同梱するべきネイティブライブラリがあるかを上記の targets では示しています.

このようにモバイル環境の場合はリンクするライブラリを示して上げる必要があるので,大変だねってことを言いたいがためにこの記事書いたものでもあります.

パッケージング

nuget pack 頑張って作ったライブラリ.nuspec

を叩いて終わり.やったー

最後に

うっわ,すっごい手抜き記事…という感じがありますが,とりあえず自分がまた読んで思い出せるレベルには書けたかなという感じです. 実際にリポジトリの中身を見て理解するほうが早いと思うので,それと照らし合わせて手を動かしてみてください.

今回の記事でやっとデスクトップで使っていたネイティブライブラリがモバイルでも使えるようになったため,更にプロダクトの幅が広がりそうです.

またこの実験をする上で非常に Travis CI や AppVeyor が役に立ちました. 今度から積極的に使っていきたいです.