Native Library Interop (旧称:Slim Bindings) 作り方編

NativeLibraryInterop_Main.jpg 前回、Slim BindingsもといNative Library Interopについての説明記事を書きました。 今回は作り方について説明していきます。

前回の記事を書いた後に名称が変更され、サンプルリポジトリが.NETのCommunityToolkit配下に移管されました。 これにより正式にコミュニティによって共有される設計パターンとなったらしく、MS Leanにもドキュメントが追加されました。

サンプルプロジェクトのリポジトリ

ドキュメント

自力でプロジェクト一式を作る

Native Library Interopではtemplateディレクトリに置かれたプロジェクト一式のテンプレートを改造して作ることが推奨されています。 せっかくなので、今回は勉強のために自力でこのプロジェクト一式の再現に挑戦してみます。なお、手順はすべてMacで実行しています。

環境準備

少なくとも以下の開発ツールが必要となります。

  • .NET SDK
  • .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用のプロジェクトを追加していきます。

androidiosディレクトリは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アプリプロジェクトを新規作成

20240805000543.png

20240805000756.png

Androidライブラリモジュールを追加

20240805000607.png

20240805000459.png

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プロジェクトを追加

20240805000928.png

20240805000944.png

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");
    }

上手くいくとこのような実行結果になります。

20240804235949.png

お気軽にご依頼・ご相談ください

前へ

Microsoft Graph APIを使ってPower BIでPlannerの情報を取得する!

次へ

【Movable Type】特定の記事を先頭に固定表示する方法(コンテンツタイプ版)