Native Library Interop (旧称:Slim Bindings) 作り方編
前回、Slim BindingsもといNative Library Interopについての説明記事を書きました。 今回は作り方について説明していきます。
前回の記事を書いた後に名称が変更され、サンプルリポジトリが.NETのCommunityToolkit配下に移管されました。 これにより正式にコミュニティによって共有される設計パターンとなったらしく、MS Leanにもドキュメントが追加されました。
サンプルプロジェクトのリポジトリ
ドキュメント
- Native Library Interop - .NET MAUI Community Toolkit - Community Toolkits for .NET | Microsoft Learn
自力でプロジェクト一式を作る
Native Library Interopではtemplate
ディレクトリに置かれたプロジェクト一式のテンプレートを改造して作ることが推奨されています。
せっかくなので、今回は勉強のために自力でこのプロジェクト一式の再現に挑戦してみます。なお、手順はすべてMacで実行しています。
環境準備
少なくとも以下の開発ツールが必要となります。
- .NET SDK
- この記事では.NET 8.0 SDKを使用しています。
- .NET 8.0 (Linux、macOS、Windows) をダウンロードする
- .NET Workload
- この記事ではiOS, Android, MAUIを使用しています。
- コマンドラインで
dotnet workload install maui
を実行すると一度にインストールできます。
- コマンドラインで
- Objective-Sharpie
- iOSのBinding Libraryプロジェクトをビルドするときに必要となります。
- https://aka.ms/objective-sharpie からダウンロードできます。
アプリケーション本体のプロジェクトの作成
まずBinding Libraryを使うアプリケーション側のプロジェクトを作成します。 既存のアプリケーションを使う場合は省略出来ます。
まず最初にMAUIアプリのプロジェクトを作ります。
dotnet new maui --name NativeLibraryInteropSampleApp
今回はiOS、Androidだけを対象にしたいので、このタイミングでcsprojファイルを編集してTargetFrameworks
からmaccatalystとwindowsを削除します。
NuGet.configの追加
Native Library Interopで使うビルドタスクが独自のパッケージフィードで配布されているため、NuGet.config
をおいてパッケージソースを追加します。
以下の内容のNuGet.config
ファイルをslnファイルと同じ階層に作成して下さい。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="maui-nativelibraryinterop" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nativelibraryinterop/nuget/v3/index.json" />
</packageSources>
</configuration>
Binding用プロジェクトの追加
Binding用のプロジェクトを追加していきます。
android
、ios
ディレクトリはslnファイルと同じ階層に作り、それぞれの中に.NETのBinding Libraryとネイティブのライブラリプロジェクトで1セットで配置します。
こんな感じのディレクトリ構造になります。
- android
- native
- Android Studioプロジェクト一式
- NativeLibraryInteropSample.Android.Binding
- .NETのBidning Libraryプロジェクト一式(Android)
- ios
- native
- Xcodeプロジェクト一式
- NativeLibraryInteropSample.iOS.Binding
- .NETのBidning Libraryプロジェクト一式(iOS)
ネイティブプロジェクトの置き場所と途中のディレクトリを作っておきます。(slnファイルと同じ階層で実行します)
mkdir -p android/native
mkdir -p ios/native
.NETのBinding Libraryプロジェクトを作り、slnファイルにも追加します。(slnファイルと同じ階層で実行します)
dotnet new android-bindinglib --name NativeLibraryInteropSample.Android.Binding --output android/NativeLibraryInteropSample.Android.Binding
dotnet new iosbinding --name NativeLibraryInteropSample.iOS.Binding --output ios/NativeLibraryInteropSample.iOS.Binding
必要な.NETプロジェクトが揃ったので、ここでソリューションファイルを作ってプロジェクトを追加しておきます。
dotnet new sln --name NativeLibraryInteropSampleApp
dotnet sln add **/*.csproj
アプリケーション本体のプロジェクトの設定を変更
最初に作ったMAUIアプリケーションのcsprojに以下の内容を追加して、各Binding用プロジェクトへの参照を追加します。
AndroidLibrary
の部分は、C#から呼び出す必要のない(JavaやKotolinで依存関係が閉じている).jar
や.aar
を取り込むための設定なようです。
実行時に依存ライブラリが見つからなくて動かない場合、Binding用プロジェクトのビルド結果から対象ファイルをアプリケーション本体のプロジェクトに取り込み直すために有効化することになると思います。
<!-- Reference to MaciOS Binding project -->
<ItemGroup Condition="$(TargetFramework.Contains('ios'))">
<ProjectReference Include="..\macios\NativeLibraryInteropSample.iOS.Binding\NativeLibraryInteropSample.iOS.Binding.csproj" />
</ItemGroup>
<!-- Reference to Android Binding project -->
<ItemGroup Condition="$(TargetFramework.Contains('android'))">
<ProjectReference Include="..\android\NativeLibraryInteropSample.Android.Binding\NativeLibraryInteropSample.Android.Binding.csproj" />
</ItemGroup>
<!-- Reference the Android binding dependencies -->
<!-- Uncomment the code block below and update the AndroidLibrary path to point your dependency .aar -->
<!-- <ItemGroup Condition="$(TargetFramework.Contains('android'))">
<AndroidLibrary Include="..\android\native\newbinding\bin\Release\net8.0-android\outputs\deps\{yourDependencyLibrary.aar}">
<Bind>false</Bind>
<Visible>false</Visible>
</AndroidLibrary>
</ItemGroup> -->
Android ネイティブライブラリプロジェクトの作成
Androidのネイティブライブラリプロジェクトを作っていきます。 正直なところ自力で作ると結構面倒そうなのでサンプルリポジトリのテンプレートからコピーした方が良いかもしれません。
イチから作成する場合、以下のような手順が良さそうです。
- Android Studioで空のAndroidアプリプロジェクトを新規作成。
android/native
ディレクトリの直下に展開されるようにします。
- 「Android Library」のモジュールを追加。
- アプリケーション部分を削除。
app
ディレクトリを削除。.idea
ディレクトリもいったん削除。build.gradle.kts
からid("com.android.application")
の行を削除。settings.gradle.kts
からinclude(":app")
の行を削除。- .NETのBinding用プロジェクトから、Android Stuido側のライブラリモジュールを指定できるのでアプリケーション部分を残しても大丈夫かも。
- Android Studioプロジェクトのライブラリモジュールからユニットテストを削除
android/native/{ライブラリモジュール名}/build.gradle.kts
の設定を修正- 別途説明
空のAndroidアプリプロジェクトを新規作成
Androidライブラリモジュールを追加
android/native/{ライブラリモジュール名}/build.gradle.kts
の設定を修正
まず、defaultConfig
からminSdk
以外の項目を削除します。
次に、dependencies
部分を次のように置き換えます。
copyDependencies
はラップしたいライブラリのファイルを.NETのBinding Libraryに持ってくるために必要です。
// Create configuration for copyDependencies
configurations {
create("copyDependencies")
}
dependencies {
// Add package dependency for binding library
// Uncomment line below and replace {dependency.name.goes.here} with your dependency
// implementation("{dependency.name.goes.here}")
// Copy dependencies for binding library
// Uncomment line below and replace {dependency.name.goes.here} with your dependency
// "copyDependencies"("{dependency.name.goes.here}")
}
// Copy dependencies for binding library
project.afterEvaluate {
tasks.register<Copy>("copyDeps") {
from(configurations["copyDependencies"])
into("${buildDir}/outputs/deps")
}
tasks.named("preBuild") { finalizedBy("copyDeps") }
}
例えばfaccebook SDKをラップする場合、dependencies
には次のようにセットで記述すれば良いようです。
`implementation("com.facebook.android:facebook-android-sdk:latest.release")`
"copyDependencies"("com.facebook.android:facebook-android-sdk:latest.release")
Bindingの動作確認用クラスを作成
モジュール作成時に一緒に作られていたサンプルファイルを改造して、HelloWorld的な動作確認用のクラスにします。
package com.example.nlisample;
public class SampleBinding {
public static String getString(String myString)
{
return myString + " from java!";
}
}
iOS ネイティブライブラリプロジェクトの作成
iOSのネイティブライブラリプロジェクトを作っていきます。 こちらはAndroidと違って準備が簡単です。
Xcodeで新規プロジェクトの作成からFramework
のテンプレートを選択します。
作成場所にはios/native
の配下を指定します。
Frameworkプロジェクトを追加
Bindingの動作確認用クラスを作成
XcodeでSwiftファイルを追加して、HelloWorld的な動作確認用のクラスを作っておきます。
.NETのBinding LibraryはObjective-C経由でアクセスするため@objc
が必要です。
import Foundation
@objc(SampleBinding)
public class SampleBinding : NSObject
{
@objc
public static func getString(myString: String) -> String {
return myString + " from swift!"
}
}
.NETのBinding Libraryプロジェクトを手直し
Android
android
配下のcsprojに以下の内容を追加します。
ModuleName
部分はAndroid Studioで作ったライブラリモジュールの名前にして下さい。
NLIGradleProjectReference
部分がAndroid Studioのライブラリモジュールをビルドして、.NETのBinding用プロジェクトに取り込むための設定です。
NuGet.configを追加していたのは、ここに関連するビルドタスクをNuGetパッケージとして取得するためでした。
<!-- Reference to Android project -->
<ItemGroup>
<NLIGradleProjectReference Include="../native" >
<ModuleName>{Android Studioのライブラリモジュール名}</ModuleName>
<!-- Metadata applicable to @(AndroidLibrary) will be used if set, otherwise the following defaults will be used:
<Bind>true</Bind>
<Pack>true</Pack>
-->
</NLIGradleProjectReference>
</ItemGroup>
<!-- Reference to NuGet for building bindings -->
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks" Version="0.0.1-pre1" PrivateAssets="all" />
</ItemGroup>
iOS
iOSではcsprojの編集の他に、API定義も行います。
まず、ios
配下のcsprojに以下の内容を追加します。
NLIXcodeProjectReference
部分がXcodeのFrameworkをビルドして、.NETのBinding用プロジェクトに取り込むための設定です。
NuGet.configを追加していたのは、ここに関連するビルドタスクをNuGetパッケージとして取得するためでした。
Android側と異なり、xcodeprojまでのファイルパスを具体的に指定する必要があるため、実際のパスに合わせて修正して下さい。
<!-- Reference to Xcode project -->
<ItemGroup>
<NLIXcodeProjectReference Include="../native/{プロジェクト名}/{プロジェクト名}.xcodeproj">
<SchemeName>{プロジェクト名}</SchemeName>
<SharpieNamespace>{プロジェクト名}</SharpieNamespace>
<SharpieBind>true</SharpieBind>
<!-- Metadata applicable to @(NativeReference) will be used if set, otherwise the following defaults will be used:
<Kind>Framework</Kind>
<SmartLink>true</SmartLink>
-->
</NLIXcodeProjectReference>
</ItemGroup>
<!-- Reference to NuGet for building bindings -->
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks" Version="0.0.1-pre1" PrivateAssets="all" />
</ItemGroup>
次にApiDefinition.cs
を以下のように編集して、Swiftで動作確認用に作ったクラスに対するAPI定義を追加します。
この辺りは従来のBinding Libraryの作り方となり、ここで楽をするためにSwiftで都合の良いAPIを定義するのがNative Library Interop(旧称:Slim Bindings)の要点と言えます。
using Foundation;
namespace NativeLibraryInteropSample.iOS {
// @interface SampleBinding : NSObject
[BaseType (typeof(NSObject))]
interface SampleBinding
{
// +(NSString * _Nonnull)getStringWithMyString:(NSString * _Nonnull)myString __attribute__((warn_unused_result("")));
[Static]
[Export ("getStringWithMyString:")]
string GetString (string myString);
}
}
Objective-C のバインディングに関するドキュメント
MAUIアプリで動作確認
最初に作成したMAUIアプリを改造してBindingの動作確認をします。
ちょうど良くMainPage.xaml.cs
にボタンを押した時の処理があるので、ここでネイティブライブラリのAPIを呼び出してみましょう。
private void OnCounterClicked(object sender, EventArgs e)
{
#if __IOS__
// XcodeのFrameworkで動作確認用に定義したメソッドを呼び出す。
string message = NativeLibraryInteropSample.iOS.SampleBinding.GetString("Hello");;
#else
// Android Studioのライブラリモジュールで動作確認用に定義したメソッドを呼び出す。
string message = Com.Example.Nlisample.SampleBinding.GetString("Hello");;
#endif
_ = this.DisplayAlert("Hello Native Library Interop", message, "OK");
}
上手くいくとこのような実行結果になります。
フォローしませんか?
お気軽にご依頼・ご相談ください