🍎

.NET9時代のiOS向けのNative Library Interop

2025/01/03に公開

.NET MAUIアプリでNative LibraryやNative SDKにアクセスするためのパターンとして、Native Library Interopがあります。
このNative Library Interopですが、.NET 9になり方法が少し変わりました。

2025年1月3日現在、日本語版のMicrosoft Learnの該当ページは古い記述のままになっており、その記述のとおりに実行するとリンカーが落ちるような状況になってしまいます。

本記事では翻訳前のドキュメントを元に、実際に試すことが出来るまでの流れを追います。

英語版の公式ドキュメント
https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/native-library-interop/

ドキュメントから参照されているサンプルプロジェクトリポジトリ
https://github.com/CommunityToolkit/Maui.NativeLibraryInterop/tree/main?tab=readme-ov-file

事前準備

macOSで実行していることを前提とします。

必須

  • .NET 9
  • .NET MAUI Workload

あると便利

iOS向けBindingには、Objective-Cのメソッド名などを記述する必要があります。
例えば (void)initializeWithClientName:(NSString * _Nonnull)clientName outputPortName:(NSString * _Nonnull)outputPortName destinationName:(NSString * _Nonnull)destinationName; みたいのがあれば、initializeWithClientName:outputPortName:destinationName: みたいな感じに…
わざわざ調べたりするのは面倒なので、以下のヘルパーツールを使うのが良いです。

Native Libraryのプロジェクト作成

Xcodeでプロジェクトを作成します。
MultiplatformのFrameworkやiOSのFrameworkを作成します。

今回はSampleNativeLibraryという名前でプロジェクトを作成しました。
Swiftファイルをプロジェクトに追加し、こんな感じで文字列を結合して返す関数を作ってみました。

import Foundation
import os

let logger = Logger(subsystem: "com.example.sample", category: "binding")

@objc(Sample)
public class Sample: NSObject {
    @objc
    public static func greeting(name: String) -> String {
        logger.info("User name: \(name, privacy: .public)")
        return "Hello, \(name)!"
    }
}

またプロジェクトファイルを編集し、SWIFT_INSTALL_OBJC_HEADER の項目を YES に変更します。
これがないと ${LibraryName}-Swift.h が生成されず、Objective-Sharpieでバインディングを自動生成することが出来ません。
またmacOSも対象とする場合、SUPPORTS_MACCATALYST の項目を YES に変更します。

バインディングプロジェクトの作成

実際にNative LibraryをBindingするプロジェクトを作成します。
dotnet new listでテンプレートを調べるとiosbindingというテンプレートが見つかります。
これを使ってプロジェクトを作成しましょう。

生成されたプロジェクトのcsprojファイルに以下の内容を追記します。

  <ItemGroup>
    <XcodeProject Include="relative_path_to_native_library_xcode_project/xcode_project.xcodeproj">
      <!-- Project名を入れる -->
      <SchemeName>SampleNativeLibrary</SchemeName>
      <!-- Metadata applicable to @(NativeReference) will be used if set, otherwise the following defaults will be used:
      <Kind>Framework</Kind>
      <SmartLink>true</SmartLink>
      -->
    </XcodeProject>
  </ItemGroup>

ここが日本語版のMicrosoft Learnの記述だと古くなっていたようで、リンク時に失敗していました。

Binding用のコード追加

このプロジェクトのApiDefinition.csにバインディング用のコードを追記していきます。
otool -ovでexportされているシンボルを見つけてApiDefinition.csに記述するのもいいですが、今回はObjective-Sharpieを利用して定義を生成していきます。

Objective-Sharpieを利用するには、Native Libraryのヘッダファイルが必要となります。
これを用意するために、一度Native Libraryのプロジェクトをビルドします。
Xcodeでビルドしてもよいですが、コマンドラインで行った方がプロジェクトまでのパスを記述しやすいのでコマンドラインで行います。

$ cd SampleNativeLibrary
$ ls .
SampleNativeLibrary
SampleNativeLibrary.xcodeproj
$ xcodebuild -project SampleNativeLibrary.xcodeproj build

同一ディレクトリにbuildディレクトリが生成されます。

次にObjective-Sharpieの引数に渡すための値を調べます。

$ sharpie xcode -sdks
sdk: appletvos18.2     arch: arm64
sdk: iphoneos18.2      arch: arm64   armv7
sdk: ios18.2-macabi    arch: x86_64  arm64
sdk: macosx15.2        arch: x86_64  arm64
sdk: watchos11.2       arch: armv7k  arm64

インストールされ利用可能なSDKが列挙されるので、この中のiphoneosの値を控えます。
ここでいうとiphoneos18.2です。

そして実際にコードの生成を行います。

$ sharpie bind --output=適当なディレクトリ --namespace=Bindingライブラリのnamespace --sdk=iphoneos18.2 -scope build/Release/SampleNativeLibrary.framework/Headers build/Release/SampleNativeLibrary.framework/Headers/*.h

こうすると、指定したディレクトリにApiDefinition.csが生成されます。
これをそのままBindingライブラリのファイルに置き換えると楽です。

Verify attributeなどが含まれる場合がありますが、内容が正しそうであればattributeを削除しビルドが通るようにします。

実際に出力された結果は以下のとおりです。

using Foundation;

namespace Bindingライブラリのnamespace
{
	// @interface Sample : NSObject
	[BaseType (typeof(NSObject))]
	interface Sample
	{
		// +(NSString * _Nonnull)greetingWithName:(NSString * _Nonnull)name __attribute__((warn_unused_result("")));
		[Static]
		[Export ("greetingWithName:")]
		string GreetingWithName (string name);
	}

	[Static]
	[Verify (ConstantsInterfaceAssociation)]
	partial interface Constants
	{
		// extern double SampleNativeLibraryVersionNumber;
		[Field ("SampleNativeLibraryVersionNumber", "__Internal")]
		double SampleNativeLibraryVersionNumber { get; }

		// extern const unsigned char[] SampleNativeLibraryVersionString;
		[Field ("SampleNativeLibraryVersionString", "__Internal")]
		byte[] SampleNativeLibraryVersionString { get; }
	}
}

その他の詳しいObjective-Sharpieの使い方は前述したドキュメントを参照してください。

これでBindingライブラリの準備は完了しました。
後はこれをMAUIプロジェクトから参照し利用するだけです。

ネイティブライブラリのデバッグは少々面倒なので、Swiftコードに仕込んだloggerなどをコンソールで頑張っていきましょう。

Discussion