C#
Xamarin
Xamarin.Android
nuget
Xamarin.iOS

DllImport クロスプラットフォーム nuget パッケージ作成方法(unitypackage もあるよ!)

はじめに 🤗

Xamarin その1 Advent Calendar 2017 の 8日目のエントリーです。

本記事はネイティブライブラリ用 C# バインディングライブラリの nuget パッケージ(nupkg)をクロスプラットフォームへの対応手順になります。また今回は Unity3d 用の unitypackage も作成してみます。

軽量ブロック暗号ライブラリ Naruto/simon-speck-c の C# バインディングライブラリ Naruto/simon-speck-net のクロスプラットフォーム用 nuget パッケージを作成した時の備忘録になります。

バインディングの実装の内容は薄くして、マルチ環境開発環境作りの内容を厚くしています。

nuget パッケージ作成の定石に詳しくないため、作成手順やファイル構成に間違いや改善点があればぜひコメントお願いします!

とりあえず nuget ロゴ
364px-NuGet_project_logo.svg.png

忙しい人向け🏃

先に生成物を見たい方向け。

下記で nupkg ファイルと unitypackage ファイルを作れますよ。

nupkgとunitypackageの作成
git clone --recursive git@github.com:Naruto/simon-speck-net.git
cd simon-speck-net
./scripts/create_unitypackage.sh  # (Optional) out 以下に unitypackage が作成されるよ。
./scripts/create_nuget.sh         # out/net/ 以下に nupkg が作成されるよ。

nuget.org にアップロードしたのが以下になります。
https://www.nuget.org/packages/SimonSpeckNet/

動かしてみる場合は以下のサンプルコードを使ってください。
String plainText = "test text abcdefg.";
byte[] plainByte = System.Text.Encoding.ASCII.GetBytes(plainText);

// Speck ECB mode
using (SymmetricAlgorithm algo = new Speck())
{
    algo.BlockSize = 128;
    algo.KeySize = 128;

    algo.GenerateKey();
    using (ICryptoTransform encryptor = algo.CreateEncryptor() , decryptor = algo.CreateDecryptor())
    {
        byte[] plainEnc = encryptor.TransformFinalBlock(plainByte, 0, plainByte.Length);
        byte[] plainDec = decryptor.TransformFinalBlock(plainEnc, 0, plainEnc.Length);
        Console.WriteLine(System.Text.Encoding.ASCII.GetString(plainDec));
        Console.WriteLine();
    }
}

// Speck CTR mode
using (SymmetricAlgorithm algo = new SpeckCTR())
{
    algo.BlockSize = 128;
    algo.KeySize = 128;

    algo.GenerateIV();
    algo.GenerateKey();
    using (ICryptoTransform encryptor = algo.CreateEncryptor() , decryptor = algo.CreateDecryptor())
    {
        byte[] plainEnc = encryptor.TransformFinalBlock(plainByte, 0, plainByte.Length);
        byte[] plainDec = decryptor.TransformFinalBlock(plainEnc, 0, plainEnc.Length);
        Console.WriteLine(System.Text.Encoding.ASCII.GetString(plainDec));
        Console.WriteLine();
    }
}

バインディング対象📌

今回は simon-speck-cの C# バインディングを作成します。

simon と speck は IoT などの低スペックで限られた電力の環境での動作を考慮されて提案された共通暗号鍵アルゴリズムです。
simon は半導体向けに、speck は CPU 向けにそれぞれ最適化されています。
simon-speck-c は simon と speck の暗号アルゴリズムの実装となります。(現状は speck のみの対応)
実装の特徴としては Intel AVX2ARM NEON などの SIMD を使って最適化しています。

作成する nuget パッケージのタイプ 🐔

今回作成する nuget パッケージは対応プラットフォームのネイティブライブラリをすべて包含するオールインワンパッケージとなります。

残念なことに、オールインワンパッケージ構成は nuget パッケージ的には正しくなさそうです。

openssl バインディングライブラリの構成を見てみると各プラットフォームの各アーキテクチャ毎にパッケージ化がわけられていて、おそらくこれがクロスプラットフォームのバインディングのあるべき姿なのだと思われます。

オールインワン nuget パッケージがプラットフォーム毎 nuget パッケージを比較して優れている点は「メンテナンスコストが少ないこと」。デメリットとしては「Windows でラッピング対象のネイティブライブラリを分けられない。」や「Linux のディストリビューション毎にパッケージを作成できない」というのがあります。

今回はメンテナンスコストをかけずに作成したい。そして、Windows は x64 対応のみ、Linux を泣く泣くサポート対象外にすることでデメリットを回避しています。

対象プラットフォーム 💻

今回は対応するプラットフォームは下記のとおりです。

  • Xamarin.iOS 📱
  • MonoAndroid 🤖
  • Xamarin.Mac x86/x64 🍎
  • Windows x64🍷

Linux 🐧 は distribution ごとのパッケージシステム経由でネイティブライブラリをインストールするのがベストなライブラリ管理方法ですので今回は対応対象外とします。(simon-speck-c の rpm や deb パッケージを作ったら対応検討します。)

Windows は、バインディング対象のネイティブライブラリファイルのうまい切り替え方法が思いつかなかったため x64 のみの対応としています。(AnyCPU の指定ができなくて不便なのでなんとかしたいところではあるのですが・・・)

開発環境 👩‍💻

今回は macOS 上 で Visual Studio for Mac(Xamarin Studio)を用いて開発します。
今回は macOS 上で jetbrains の Rider を用いて開発します。(※ 記述最中に Rider 2017.2 から 2017.3 へバージョンアップしたため所々バージョンの違うレイアウトが混在します。)

前提知識 🤔

ネイティブライブラリの C# バインディング対応する目的

Xamarin や .NET の機能が充実してきているのでネイティブライブラリ(しかも swift や kotlin じゃなくて C/C++ で記述された!)のバインディングが必要になるケースはほぼほぼないはずなのですが・・・下記のような場合にネイティブライブラリのバインディングが必要になります。

ネイティブ実装しかないライブラリを C# で利用したい
マルチメディア関連のライブラリの C# 実装は数が少なく、利用する際はバインディングが必要になります。
例えば libwebp や opencv、ffmpeg などなど。

C# では利用できない低レイヤーの機能を利用したい
パフォーマンスが必要なので CPU の機能、例えば Intel Quick Sync Video や Intel AVX2, arm NEON などを利用する場合にバインディングが必要になります。考えてみると、上記であげた libwebp や opencv、ffmpeg もパフォーマンス向上のために SIMD を使ってますね。

過去のリソースを修正せずに C# で利用したい
私はこのケースに出会ったことがないのですが、Win32 API で作られた .dll ファイルと .h ファイルがあるが、ソースがない場合などでしょうか。
ロストテクノロジー化したけど、モダンな環境でメンテナンスしていきたい場合に C# バインディングして再利用していくなどあるかもしれません。

simon-speck-net を C# バインディング対応した理由
理由は二つありました。

一つ目は他プログラミング言語対応は暗号アルゴリズムのロジックを一カ所にまとめてメンテナンスコストを増やしたくない考えがありました。今後言語が増えるたびにロジックを書いていたらメンテナンスコストが指数的に増えていきます。

二つ目は simon-speck-c 側で SIMD 命令(intel avx2, arm neon) の実装をしていたためです。SIMD を使うと 2-4倍ぐらいは高速化が可能です。わざわざこのパフォーマンスを捨てて C# で実装する理由がありませんでした。

以上から、C# で暗号アルゴリズムのロジックを実装せずに、C# バインディングにすることに決めました。

C# にはネイティブライブラリの API を C# から実行する仕組み「プラットフォーム呼び出しサービス」が用意されています。

プラットフォーム呼び出しサービス (P/Invoke: Platform Invocation Services)

C# 側でネイティブライブラリ API と同じ返り値型と引数で関数を宣言し、その宣言した関数へ DllImport 属性を付与すると、C# からネイティブライブラリ API を実行できるようになります。

例えば、下記は実行中プロセスのプロセス ID を返す getpid を宣言したものになります。getpid は返り値型は pid_t(int の typedef) で引数がありません。C# 側で宣言すると下記のようになります。

getpidバインディング
[DllImport ("libc.so")]
private static extern int getpid ();

​​
上記 API を C# 側で実行するとプロセス ID が返ってきます。

DllImport は C 言語の関数のバインディングしか実装できないので、C++ で実装したネイティブライブラリの API のプラットフォーム呼び出しをする場合は extern "C" { } で囲むように注意してください。

P/Invoke の詳細な解説は Xamarin のバックエンドの mono のドキュメント ( http://www.mono-project.com/docs/advanced/pinvoke/ ) が参考になります。

各プラットフォームの DllImport でのネイティブライブラリ指定方法

各プラットフォームでバインディングが可能なネイティブライブラリタイプと指定方法は以下の通りです。

プラットフォーム タイプ DllImport の指定方法 ファイル名 備考
Xamarin.iOS 📱 静的ライブラリ DllImport("__Internal") libname.a fat library の利用可能
MonoAndroid 🤖 動的ライブラリ DllImport("name") libname.so アーキテクチャ毎のバイナリが必要。
Xamarin.Mac 🍎 動的ライブラリ DllImport("name") libname.dylib fat library の利用可能
Windows🍷 動的ライブラリ DllImport("name") name.dll アーキテクチャ毎のバイナリが必要。

静的ライブラリは DllImport("__Internal") を指定し、動的ライブラリはライブラリファイルから拡張子と lib 文字列を抜いたもので指定できます。例えば、 macOS の libname.dylib や Linux(Android) の libname.so や Windows のname.dllDllImport("name") で指定できます。

この仕様はバインディングライブラリのソリューションの構成にも影響してきます。

事前準備 🚚

各プラットフォームのネイティブライブラリのバイナリを用意します。
各プラットフォームのビルド環境の構築手順は省略します。具体的なビルド手順は simon-speck-c のビルド手順を記載ます。

iOS 📱

静的ライブラリ(.a)を用意します。
静的ライブラリは armv7a, arm64, x86, x86_64 用のライブラリを含んだ fat ライブラリであることが望ましいです。(iOS11 以降 arm64 バイナリのみのサポートになりましたが、 iOS10 のサポート期間が終わるまでは armv7a バイナリを入れた方が良いでしょう。また、x86x86_64 を入れてるのはシミュレータのためです。)

全アーキテクチャの静的ライブラリを作成後、 libtool でfat ライブラリファイルを生成します。

iOS用fatライブラリの作成
libtool -static ../build_iphone64/${LIBNAME} ../build_iphone/${LIBNAME} ../build_iphones/${LIBNAME} ../build_sim/${LIBNAME} ../build_sim64/${LIBNAME} -o ./${LIBNAME}

ビルドスクリプトは下記になります。
https://github.com/Naruto/simon-speck-c/blob/develop/scripts/speck/build_ios.sh

Android 🤖

動的ライブラリ(.so)を用意します。
動的ライブラリは armeabi, armeabi-v7a, arm-v8a, x86, x86_64 アーキテクチャのそれぞれの動的ライブラリがあることが望ましいです。(といいつつ、もう armeabi, x86x86_64 はもう不要では・・・と思っています。)

cmake v3.7 から正式に Android NDK 用ライブラリのビルドがサポートされました。ビルド時に -DCMAKE_ANDROID_ARCH_ABI= でアーキテクチャ指定するとそれぞれのアーキテクチャのライブラリをビルド可能です。

Android プロジェクトの libs 以下と同じディレクトリ構造にします。すなわちネイティブライブラリはアーキテクチャ名のディレクトリへ配置します。

Androidの各アーキテクチャのライブラリを作成する
android/
|-- arm64-v8a
|   `-- libspeck.so
|-- armeabi
|   `-- libspeck.so
|-- armeabi-v7a
|   `-- libspeck.so
|-- x86
|   `-- libspeck.so
`-- x86_64
    `-- libspeck.so

ビルドスクリプトは下記になります。
https://github.com/Naruto/simon-speck-c/blob/develop/scripts/speck/build_android.sh

macOS 🍎

動的ライブラリ(.dylib)と bundle ファイル(.bundle)を用意します。動的ライブラリは x86x86_64 アーキテクチャの fat ライブラリにしておくとベターです。

全アーキテクチャの動的ライブラリを作成後、 lipo で fat ライブラリファイルを生成します。

macOS用fatライブラリの作成
lipo -create ../build_x86_64/${LIBNAME} ../build_i386/${LIBNAME} -o ./${LIBNAME}

bundle ファイルは上記の動的ライブラリファイルから作成します。
speck.bundle/Contents/MacOS 以下に libspeck.dylib を bundle ファイル名と同じ名前 speck で配置すれば完成です。

macOS用bundleファイルの作成
BUNDLENAME=speck
/bin/mkdir -p ${BUNDLENAME}.bundle/Contents/MacOS
/bin/cp /path/to/libpseck.dylib ${BUNDLENAME}.bundle/Contents/MacOS/${BUNDLENAME}

ビルドスクリプトは下記になります。
https://github.com/Naruto/simon-speck-c/blob/develop/scripts/speck/build_mac.sh

windows 🍷

動的ライブラリ(.dll)を用意します。
動的ライブラリは x86_64 アーキテクチャのみを用意します。

https://github.com/Naruto/simon-speck-c/blob/develop/scripts/speck/build_win.bat
​​

ライブラリ配置

できあがったライブラリ群はビルドして配置します。配置構成は下記の通り。

ネイティブライブラリの配置
plugins
|-- Android
|   `-- libs
|       |-- arm64-v8a
|       |   `-- libspeck.so
|       |-- armeabi
|       |   `-- libspeck.so
|       |-- armeabi-v7a
|       |   `-- libspeck.so
|       |-- x86
|       |   `-- libspeck.so
|       `-- x86_64
|           `-- libspeck.so
|-- iOS
|   `-- libspeck.a
`-- x64
    |-- libspeck.dylib
    |-- speck.bundle
    |   `-- Contents
    |       `-- MacOS
    |           `-- speck
    `-- speck.dll

各プラットフォームのビルド&配置用スクリプトは下記になります。

https://github.com/Naruto/simon-speck-net/blob/develop/scripts/plugins/deploy_android.sh
https://github.com/Naruto/simon-speck-net/blob/develop/scripts/plugins/deploy_ios.sh
https://github.com/Naruto/simon-speck-net/blob/develop/scripts/plugins/deploy_mac.sh
https://github.com/Naruto/simon-speck-net/blob/develop/scripts/plugins/deploy_win.bat

C# プロジェクトの作成 🛠️

C# ライブラリを作成するソリューションを作成します。

ソリューションには動的ライブラリ用の C# ライブラリプロジェクトと、静的ライブラリ用の C# ライブラリプロジェクトを追加します。また、バインディングライブラリのデバッグ用コンソールアプリケーションプロジェクトも追加します。

ソリューションの作成と動的ライブラリ用の C# ライブラリプロジェクトの作成

New Solution ダイヤログで .NET の Class Library を選んでソリューション名を決定します。

プロジェクトの構造は以下の通り。
ソリューション名ディレクトリ SimonSpeckNet の中にソリューションファイル SimonSpeck.sln と プロジェクトディレクトリ SimonSpeckNet/SimonSpeckNet が作成されます。

SimonSpeckNetソリューションディレクトリ
SimonSpeckNet/
|-- SimonSpeckNet/
|     |-- ...
|     `-- SimonSpeckNet.cproj
`-- SimonSpeckNet.sln

名前が紛らわしいのでソリューションディレクトリを net に変更します。(.NET なので、net にしましたが、もしかしたら定石な名称が存在する?)

SimonSpeckNetソリューションディレクトリ(ディレクトリ名変更)
net/
|-- SimonSpeckNet/
|     |-- ...
|     `-- SimonSpeckNet.cproj
`-- SimonSpeckNet.sln

ネイティブライブラリの配置とプロジェクトへの追加

事前準備で作成したネイティブライブラリ群をソリューションファイルと同ディレクトリへ配置します。

SimonSpeckNetソリューションディレクトリ(plugins追加)
net/
|-- SimonSpeckNet/
|-- SimonSpeckNet.sln
`-- plugins/             # <- これ

SimonSpeckNet.cproj ファイルへネイティブライブラリを追加します。プロジェクトファイルを右クリックして “Add Existing Item” をクリックしてください。

追加するライブラリを選びます。ここで追加するライブラリは C# バインディングの動作確認に利用するものです。今回は macOS での開発なので libspeck.dylib を選択します。

追加した dylib ファイルを右クリック → “Properties” を選んでください。"Editable" -> "Copy to output directory:" を “Copy if newer” を選択してください。

SimonSpeckNet を右クリック → “Build Selected Projects” をクリックし、プロジェクトのビルド後、 SimonSpeckNet.dll と同ディレクトリに libspeck.dylib ファイルがコピーされていれば正しく設定できています。

静的ライブラリ用のプロジェクトの作成

静的ライブラリは動的ライブラリと同じソースファイルを利用して生成します。そのため静的ライブラリのプロジェクトは動的ライブラリのソースを参照するだけのプロジェクトにします。(新規にプロジェクトを追加するほかに、define で分けてビルドする方法があるなら知りたいです。)

ソリューションを右クリックでプロジェクトを追加を選びます。

名前は何でもいいんですが、わかりやすいように SimonSpeckNetStatic とします。

静的ライブラリ判別用 define 宣言を追加

SimonSpeckNetStatic.csproj の DefineConstants へ静的ライブラリビルド用の define 宣言 LIB_SATATIC を追加します。(LIB_STATICコーディング時の DllImport の処理を分けるのに利用します。)

SimonSpeckNetStatic.csprojへLIB_STAITCを追加
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    ...
    <DefineConstants>DEBUG;TRACE;LIB_STATIC</DefineConstants>
    ...
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    ...
    <DefineConstants>TRACE;LIB_STATIC</DefineConstants>
    ...
  </PropertyGroup>

動的ライブラリのソースファイルを参照

SimonSpeckNetStatic プロジェクトの Class1.cs を削除し、右クリック → “Add Existing Items” で SimonSpeckNet のソースファイルを指定します。SimonSpeckNetStatic.csproj で cs ファイルのパスが SimonSpeckNet ものを指定していれば成功です。

SimonSpeckNetStatic.csprojのファイルがSimonSpeckNetプロジェクトのソースファイルを参照していること
  <ItemGroup>
    <Compile Include="..\SimonSpeckNet\Class1.cs">
      <Link>Class1.cs</Link>
    </Compile>
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>

デバッグ用プロジェクトの作成

C# バインディング API が正しくバインディングされてるか確認するため、C# ライブラリを参照するコマンドラインアプリケーションのプロジェクトを追加します。

ソリューションを右クリックでプロジェクトを追加を選びます。

Console Application を選択し、プロジェクトを名を設定します。
これまた、名前は何でもいいんですがデバッグ用なので SimonSpeckNetTest などにします。

SimonSpeckNetTest を右クリック → “Add Reference” を選択し、SimonSpeckNet を選択してください。

これで、SimonSpeckNetTest をビルドした際に SimonSpeckNet のファイルもまとめて配置されていれば成功です。

ディレクトリ構成

最終的なプロジェクトのディレクトリ構成は下記となります。

最終的なSimonSpeckNetのディレクトリ構成
net/
|-- SimonSpeckNet/
|-- SimonSpeckNetStatic/
|-- SimonSpeckNetTest/
|-- SimonSpeckNet.sln
`-- plugins/

コーディング ✍️

C# のバインディングライブラリを実装します。

DllImport でのライブラリ名の指定方法

DllImport 属性で指定するライブラリ名の宣言を下記のようにします。
DllImport で指定するライブラリ名は private const string で宣言した変数を利用します。
変数宣言は #if LIB_STAITCマクロで静的ライブラリ用と動的ライブラリ用の二つに分けるようにします。(静的ライブラリ判別用 define 宣言を追加で追加したマクロを使用します。)

静的ライブラリは __Internal を指定、動的ライブラリは speck と指定します。(指定名はこちらを参照してください。)

動的ライブラリと静的ライブラリでDllImportの指定を分ける
#if LIB_STATIC
private const string LibraryName = "__Internal";
#else
private const string LibraryName = "speck";
#endif

[DllImport(LibraryName)]  
private static extern IntPtr speck_init(int _type, byte[] key, int key_len);

コーディング方針

DllImport API をそのまま利用するのは利用者に負担がかかるのため、 C# の API スタイルに合わせてラッピングしましょう。

例えば、simon-speck-net は共通鍵ブロック暗号なので、System.Security.Cryptography.SymmetricAlgorithm の派生クラス SpeckSpeckCTR を実装しています。(SpeckCTRSymmetricAlgorithmCTR モードの指定方法がないので CTR モード専用に派生させたクラスになります。)

C# の基底クラスから派生させることで、利用者の負担が軽減するばかりではなく、既存機能の差し替えが楽になります。
是非、郷には入れば郷に従うの精神で DllImport API をラッピングしましょう。

コーディング時の注意方法

C# のバインディングを記述する上での基本的な原理原則は「Interop with Native Libraries
( http://www.mono-project.com/docs/advanced/pinvoke/ )」が参考になります。

ビルドとデバッグ 🔨

CLI でのビルド手順とデバッグ手順は下記になります。

xbuildでソリューションをビルドし、CLIコマンドを実行する。
xbuild /p:TargetFrameworkVersion="v4.5" /p:Configuration=Debug
mono SimonSpeckNetTest/bin/Debug/SimonSpeckNetTest.exe

nuget.exe の用意

nupkg を生成するコマンドを取得し、プロジェクトディレクトリ内に格納します。nuget.org からダウンロードできます。

nuget.exeバイナリを取得
mkdir -p NuGet
pushd NuGet
curl -Lsfo nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
popd

プロジェクト内に nuget ディレクトリが作成され、その中に nuget.exe ファイルが格納されます。

NuGet関連のファイルを追加
net/
|-- SimonSpeckNet/
|-- SimonSpeckNetStatic/
|-- SimonSpeckNet.sln
|-- NuGet/
|     `-- nuget.exe
`-- plugins/

nuget.exe を実行するとバージョン番号と usage が表示されます。

nuget.exeの動作確認
mono NuGet/nuget.exe
NuGet Version: 4.4.1.4656
...

nuspec ファイルの作成 📃

テンプレート生成

nuspec は nupkg を生成するためのファイルになります。

nuget の spec コマンドで nuspec ファイルのテンプレートが生成されます。

nupkg用のnuspecファイルの生成
pushd NuGet
mono nuget.exe spec SimonSpeckNet
popd

メタデータ記述

パッケージ名やバージョン、著者名などなど <metadata> タグ内のパッケージ情報を記載していきます。

各タグの説明は下記を参照してください。
https://docs.microsoft.com/en-us/nuget/schema/nuspec

SimonSpeckNet の 記述は以下になります。

SimonSpeckNet.nuspecのmetadataの抜粋
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>SimonSpeckNet</id>
    <version>0.3</version>
    <authors>Naruto TAKAHASHI</authors>
    <owners>Naruto TAKAHASHI</owners>
    <licenseUrl>https://opensource.org/licenses/MIT</licenseUrl>
    <projectUrl>https://github.com/Naruto/simon-speck-net</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>SIMON and SPECK are families of lightweight block ciphers; those block ciphers are efficient and provide high performances across a wide range of devices. This package includes classes that allow you to encrypt and decrypt using native libraries that are using SIMD such as AVX2 or NEON.</description>
    <releaseNotes>- version up simon-speck-c to v0.6
- change SpeckCTR default padding mode to `Padding.None`
- SpeckCTR allows any length data when setting `Padding.None`. And return values length is same as input data length.
    </releaseNotes>
    <copyright>Copyright 2017 Naruto TAKAHASHI</copyright>
    <tags>simon speck cryptography crypto security encryption lightweight symmetric Xamarin iOS Android macOS</tags>
  </metadata>
  ...

targets ファイル作成

nuspec の続きを書く前に、各プラットフォームの targets ファイルを記載します。targets ファイルはビルドに必要なファイル情報を記載するファイルとなります。今回はネイティブライブラリの情報を記載します。

Common targets ファイル

windows と macOS 両用の targets ファイル NuGet/SimonSpeckNet.targets の内容は下記になります。windows では .dll ファイルを指定し、macOS では .dylib ファイルを指定します。

<None> タグの Include 属性値には nupkg へパッケージング後のファイルパスを記載します。
windows のネイティブライブラリは runtimes/win-x64/native/speck.dll以下に格納し、macOS のネイティブライブラリは runtimes/osx-x64/native/libspeck.dylib 以下に格納します。

NuGet/SimonSpeckNet.targets
<?xml version="1.0"  encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <!-- Windows -->
    <None Include="$(MSBuildThisFileDirectory)..\runtimes\win-x64\native\speck.dll">
      <Link>speck.dll</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>

    <!-- macOS -->
    <None Include="$(MSBuildThisFileDirectory)..\runtimes\osx-x64\native\libspeck.dylib">
      <Link>libspeck.dylib</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

iOS target ファイル

XamarioniOS 用の targets ファイル NuGet/iOS/SimonSpeckNet.targets の内容は下記になります。

<NativeReference> タグの Include 属性値には nupkg へパッケージング後のファイルパスを記載します。
iOS のネイティブライブラリは native/ios/libspeck.a 以下に格納します。

NuGet/iOS/SimonSpeckNet.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <NativeReference Include="$(MSBuildThisFileDirectory)\..\..\native\ios\libspeck.a">
      <Kind>Static</Kind>
      <ForceLoad>True</ForceLoad>
    </NativeReference>
  </ItemGroup>
</Project>

Android target ファイル

MonoAndroid 用の targets ファイル NuGet/Android/SimonSpeckNet.targets の内容は下記になります。

<AndroidNativeLibrary> タグの Include 属性値には nupkg へパッケージング後のファイルパスを記載します。
Android のネイティブライブラリは native/android 以下に格納します。各アーキテクチャ毎の設定が必要になるので全部(arm, armeabi-v7a, arm64-v8a, x86, x86_64)追加します。

NuGet/Android/SimonSpeckNet.targets
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <AndroidNativeLibrary Include="$(MSBuildThisFileDirectory)\..\..\native\android\armeabi\libspeck.so">
      <Link>Libs\armeabi\libspeck.so</Link>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="$(MSBuildThisFileDirectory)\..\..\native\android\armeabi-v7a\libspeck.so">
      <Link>Libs\armeabi-v7a\libspeck.so</Link>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="$(MSBuildThisFileDirectory)\..\..\native\android\arm64-v8a\libspeck.so">
      <Link>Libs\arm64-v8a\libspeck.so</Link>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="$(MSBuildThisFileDirectory)\..\..\native\android\x86\libspeck.so">
      <Link>Libs\x86\libspeck.so</Link>
    </AndroidNativeLibrary>
    <AndroidNativeLibrary Include="$(MSBuildThisFileDirectory)\..\..\native\android\x86_64\libspeck.so">
      <Link>Libs\x86_64\libspeck.so</Link>
    </AndroidNativeLibrary>
  </ItemGroup>
</Project>

ファイル情報記述

nupkg ファイルに格納するファイルの情報を記述します。

具体的には C# ライブラリとネイティブライブラリと targets ファイルの情報を記載します。

各ファイル毎に <file> タグに記述します。src 属性値で指定するパスは nupkg 実行時の作業ディレクトリからの相対パス、dst 属性値で指定するパスは nupkg のルートディレクトリからの相対パスとなります。(nupkg コマンドははパッケージ作成はソリューションファイルの格納ディレクトリでの実行を想定して設定します。)

C# ライブラリの配置設定は以下の通り。動的ライブラリ用の C# ライブラリを lib/net, lib/netcore, lib/win, lib/netstandard 以下に配置します。(lib/win はいらないかもしれません。)

SimonSpeckNet.nuspecのC#ライブラリ指定抜粋
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/net" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/netcore" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/win" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/netstandard" />

common 用の targets ファイルとネイティブライブラリの設定を記載します。
speck.dlllibspeck.dylibnuget/SimonSpeckNet.targets へ記述したパスへ配置します。

SimonSpeckNet.nuspecのネイティブライブラリ指定抜粋
    <file src="NuGet/SimonSpeckNet.targets" target="build/" />
    <file src="plugins/x64/speck.dll" target="runtimes/win-x64/native" />
    <file src="plugins/x64/libspeck.dylib" target="runtimes/osx-x64/native" />

iOS 用の targets ファイルとネイティブライブラリの設定を記載します。
iOS の場合は静的ライブラリ用バインディングライブラリが必要なので、
iOS の場合は動的ライブラリ用の C# ライブラリが必要なので SimonSpeckNetStatic の成果物の SimonSpeckNetStatic.dll を利用します。
libspeck.anuget/iOS/SimonSpeckNet.targets へ記述したパスへ配置します。

SimonSpeckNet.nuspecのiOSネイティブライブラリ指定抜粋
    <!-- iOS -->
    <file src="NuGet/iOS/SimonSpeckNet.targets" target="build/XamariniOS"/>
    <file src="SimonSpeckNetStatic/bin/Release/SimonSpeckNetStatic.dll" target="lib/XamariniOS/SimonSpeckNet.dll"/>
    <file src="plugins/iOS/libspeck.a" target="native/ios"/>

Android 用の targets ファイルとネイティブライブラリの設定を記載します。
各アーキテクチャの libspeck.sonuget/Android/SimonSpeckNet.targets へ記述したパスへ配置します。

SimonSpeckNet.nuspecのAndroidネイティブライブラリ指定抜粋
    <!-- Android -->
    <file src="NuGet/Android/SimonSpeckNet.targets" target="build/MonoAndroid"/>
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/MonoAndroid"/>
    <file src="plugins/Android/libs/armeabi/libspeck.so" target="native/android/armeabi" />
    <file src="plugins/Android/libs/armeabi-v7a/libspeck.so" target="native/android/armeabi-v7a" />
    <file src="plugins/Android/libs/arm64-v8a/libspeck.so" target="native/android/arm64-v8a" />
    <file src="plugins/Android/libs/x86/libspeck.so" target="native/android/x86" />
    <file src="plugins/Android/libs/x86_64/libspeck.so" target="native/android/x86_64" />

完成 nuspec ファイル

完成した nuspec ファイルは以下になります。
NuGet/SimonSpeckNet.nuspec
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
  <metadata>
    <id>SimonSpeckNet</id>
    <version>0.3</version>
    <authors>Naruto TAKAHASHI</authors>
    <owners>Naruto TAKAHASHI</owners>
    <licenseUrl>https://opensource.org/licenses/MIT</licenseUrl>
    <projectUrl>https://github.com/Naruto/simon-speck-net</projectUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>SIMON and SPECK are families of lightweight block ciphers; those block ciphers are efficient and provide high performances across a wide range of devices. This package includes classes that allow you to encrypt and decrypt using native libraries that are using SIMD such as AVX2 or NEON.</description>
    <releaseNotes>- version up simon-speck-c to v0.6
- change SpeckCTR default padding mode to `Padding.None`
- SpeckCTR allows any length data when setting `Padding.None`. And return values length is same as input data length.
    </releaseNotes>
    <copyright>Copyright 2017 Naruto TAKAHASHI</copyright>
    <tags>simon speck cryptography crypto security encryption lightweight symmetric Xamarin iOS Android macOS linux</tags>
  </metadata>
  <files>
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/net" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/netcore" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/win" />
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/netstandard" />

    <file src="NuGet/SimonSpeckNet.targets" target="build/" />
    <file src="plugins/x64/speck.dll" target="runtimes/win-x64/native" />
    <file src="plugins/x64/libspeck.dylib" target="runtimes/osx-x64/native" />
    <file src="plugins/x64/libspeck.so" target="runtimes/linux-x64/native" />

    <!-- Android -->  
    <file src="NuGet/Android/SimonSpeckNet.targets" target="build/MonoAndroid"/>
    <file src="SimonSpeckNet/bin/Release/SimonSpeckNet.dll" target="lib/MonoAndroid"/>
    <file src="plugins/Android/libs/armeabi/libspeck.so" target="native/android/armeabi" />
    <file src="plugins/Android/libs/armeabi-v7a/libspeck.so" target="native/android/armeabi-v7a" />
    <file src="plugins/Android/libs/arm64-v8a/libspeck.so" target="native/android/arm64-v8a" />
    <file src="plugins/Android/libs/x86/libspeck.so" target="native/android/x86" />
    <file src="plugins/Android/libs/x86_64/libspeck.so" target="native/android/x86_64" />

    <!-- iOS -->
    <file src="NuGet/iOS/SimonSpeckNet.targets" target="build/XamariniOS"/>
    <file src="SimonSpeckNetStatic/bin/Release/SimonSpeckNetStatic.dll" target="lib/XamariniOS/SimonSpeckNet.dll"/>
    <file src="plugins/iOS/libspeck.a" target="native/ios"/>
  </files>
</package>

(長いので折りたたみ)

ファイル自体は下記のものになります。
https://github.com/Naruto/simon-speck-net/blob/develop/net/NuGet/SimonSpeckNet.nuspec

その他

そのほかの nuspec の仕様(nupkg の依存関係など)や記述方法については、下記ドキュメントが参考になります。

https://docs.microsoft.com/en-us/nuget/schema/nuspec

nupkg の作成 🎁

ようやく、nupkg が作成できます。
C# ライブラリをビルドした後に nupkg を作成します。

nupkg作成方法
xbuild /p:TargetFrameworkVersion="v4.5" /p:Configuration=Release
mono NuGet/nuget.exe pack nuget/SimonSpeckNet.nuspec -BasePath $(pwd)

上記に加えてパッケージング前に NUnit でユニットテストを実行する仕組みにしたスクリプトが下記になります。

https://github.com/Naruto/simon-speck-net/blob/develop/scripts/create_nuget.sh

nupkg 動作確認 🐛

nuget にアップロードする前にパッケージが正しく動作するか確認します。

パッケージの設定

nuget リポジトリはローカルのディレクトリパスを指定できます。nuget パッケージのデバッグする際に活用しましょう。

メニューの Tools → NuGet → Show NuGet Sources を選んで

フィードに file://ディレクトリパス を追加してください。ここでは $HOME/projects/simon-speck-net/net 以下に nupkgファイル があるものと設定しています。

あとは、プロジェクトファイルの “Manage NuGet Packages” などで探し出して入れてみてください。
検索しなくても nupkg ファイルが見つかるはずです。

後は各それぞれのプラットフォームのプロジェクトを作成し、パッケージを入れてちゃんとビルドできるか確認しましょう。

  • Mac
  • Windows
  • iOS
  • Android

nuget.org へのアップロード(Optional)

あとは nuget.org へアップロードするだけですね。

nuget.orgへのnupkgのアップロード方法
mono ./NuGet/nuget.exe push SimonSpeckNet-XXX.nupkg

アップロードした結果はこんな感じ。 meta データに期待した内容が反映されていますね。

https://www.nuget.org/packages/SimonSpeckNet/

スクリーンショット 2017-12-03 20.31.31.png

完成です。
お疲れ様でした。

unitypackage の作成(optional)

各プラットフォームのネイティブライブラリと C# ライブラリのソースが揃っていると、Unity3d 用のパッケージ unitypackage ファイルも作成可能になります。

Unity ではネイティブライブラリのことをネイティブプラグインを名称しておりまして、Unity の DllImport 周りの話は「ネイティブプラグイン」で検索すれば出てくるかと思われます。とりあえず Unity オフィシャルのネイティブプラグイン関連の内容は下記になります。

https://docs.unity3d.com/ja/current/Manual/NativePlugins.html
https://docs.unity3d.com/ja/current/Manual/PluginsForDesktop.html

忙しい人向け

simon-speck-net の unitypackage 作成手順をスクリプト化したものは下記になります。

https://github.com/Naruto/simon-speck-net/blob/develop/scripts/create_unitypackage.sh

ネイティブライブラリネイティブプラグインの用意

実はライブラリ配置は Unity プロジェクトを意識した配置にしていて、そのまま利用できます。

現行の Unity は下記ネイティブライブラリをサポートしていないため削除します。

  • Android
    • arm64-v8a
    • armeabi
    • x86_64
  • macOS
    • .dylib (代わりに .bundle を利用する)

C# ライブラリの用意

C# ソースをビルドします。
xbuild は使わずに Unity 内部の MonoBleedingEdge/bin/mcs コンパイラを直接利用します。

Unity用C#ライブラリのビルド方法
UNITY_APP=/Applications/Unity/Unity.app
src_array=`find ${BASEDIR}/net/SimonSpeckNet/Speck -name '*.cs'`

COMPILER=${UNITY_APP}/Contents/MonoBleedingEdge/bin/mcs
${COMPILER} -sdk:2 -r:${UNITY_APP}/Contents/Managed/UnityEngine.dll -r:${UNITY_APP}/Contents/Managed/UnityEditor.dll -target:library -optimize+ -out:SimonSpeckNet_Static.dll -unsafe+ -define:LIBSPECK_STATIC ${src_array}
${COMPILER} -sdk:2 -r:${UNITY_APP}/Contents/Managed/UnityEngine.dll -r:${UNITY_APP}/Contents/Managed/UnityEditor.dll -target:library -optimize+ -out:SimonSpeckNet.dll -unsafe+ ${src_array}

ターゲット SDK は Unity Player で利用している 2.0 を指定。
念のため Unity Player のランタイムライブラリ UnityEngine.dll と Unity Editor のランタイムライブラリ UnityEditor.dll を参照します。
静的ライブラリ用と動的ライブラリの二つ作るのを忘れずに。

ライブラリ配置

ネイティブライブラリネイティブプラグインと C# ライブラリが用意できたので配置していきます。
unitypackage パッケージを作成する一時ディレクトリ build_unity の中に配置していきます。

Assets/Plugins以下の配置
build_unity/Assets/Plugins/
|-- Android
|   |-- SimonSpeckNet.dll
|   `-- libs
|       |-- armeabi-v7a
|       |   `-- libspeck.so
|       `-- x86
|           `-- libspeck.so
|-- iOS
|   |-- SimonSpeckNet_Static.dll
|   `-- libspeck.a
`-- x64
    |-- SimonSpeckNet.dll
    |-- speck.bundle
    |   `-- Contents
    |       `-- MacOS
    |           `-- speck
    `-- speck.dll

Asset/Plugins/プラットフォーム名 に配置していきます。
Android は Asset/Plugins/Android。iOS は Asset/Plugins/iOS
Windows と macOS は Asset/Plugins/x64 に配置します。

配置をしたら Unity Editor で build_unity ディレクトリを開いて .meta データを生成します。

unitypackage パッケージ作成

unitypackage は unity バッチモードにて -exportPackage オプションで指定して作成します。

unitypackage作成方法
${UNITY_APP}/Contents/MacOS/Unity -quit -batchmode -nographics -projectPath $(pwd) -logFile /dev/stdout -exportPackage Assets/Plugins SimonSpeckForUnity.unitypackage

SimonSpeckForUnity.unitypackage が生成されます。

まとめ

今までの手順を簡潔にまとめると下記になります。

  1. 各プラットフォームのネイティブライブラリのバイナリを用意する
  2. ネイティブライブラリを DllImport した C# コードを用意する。動的ライブラリ、静的ライブラリどちらにも対応しておくこと。
  3. 各プラットフォームのネイティブライブラリの参照情報を記述した targets ファイルを作成する
  4. nuspec で ネイティブライブラリ, C# ライブラリ, targets ファイルを固めて nupkg を作成する
  5. (Optional) nuget.org にアップロード
  6. (Optional) unitypackage を作成する

nuget.org へアップロードすると場所やプラットフォームを気にせずに利用可能になるため是非是非アップロードをオススメします。

課題はまだまだ多いですが、以上がネイティブライブラリ用 C# バインディングライブラリの nuget パッケージを作る手順になります。Linux の対応を入れていないのは致命的なので、Linux 対応を進めてまた記事を記載しようと思います。

最後まで読んでいただきありがとうございました。 🙇