メインコンテンツまでスキップ

Kotlin/NativeをApple frameworkとして利用する – チュートリアル

注記

Objective-C ライブラリのインポートは試験段階です。 cinteropツールによってObjective-Cライブラリから生成されたすべてのKotlinの宣言には、@ExperimentalForeignApi アノテーションが必要です。

Kotlin/Native に同梱されているネイティブプラットフォームライブラリ(Foundation、UIKit、POSIXなど)は、一部のAPIでのみオプトインが必要です。

Kotlin/Nativeは、Swift/Objective-Cとの双方向の相互運用性を提供します。Objective-CのフレームワークやライブラリをKotlinのコードで使用することも、KotlinのモジュールをSwift/Objective-Cのコードで使用することもできます。

Kotlin/Nativeには、事前にインポートされたシステムフレームワークのセットが付属しています。既存のフレームワークをインポートしてKotlinから使用することもできます。このチュートリアルでは、独自のフレームワークを作成し、macOSおよびiOS上のSwift/Objective-CアプリケーションからKotlin/Nativeのコードを使用する方法を学びます。

このチュートリアルでは、以下の内容を学びます。

コマンドラインを使用してKotlinのフレームワークを生成できます。直接またはスクリプトファイル(.sh.batファイルなど)を使用します。 ただし、このアプローチは、数百のファイルとライブラリを持つ大規模なプロジェクトには適していません。 ビルドシステムを使用すると、Kotlin/Nativeコンパイラバイナリと推移的な依存関係を持つライブラリをダウンロードしてキャッシュしたり、コンパイラとテストを実行したりすることで、プロセスを簡素化できます。 Kotlin/Nativeは、Kotlin Multiplatform pluginを介してGradleビルドシステムを使用できます。

Macを使用しており、iOSまたはその他のAppleターゲット用のアプリケーションを作成および実行する場合は、最初にXcode Command Line Toolsをインストールし、起動して、ライセンス条項に同意する必要があります。

Kotlinライブラリを作成する

ヒント

詳しい最初の手順については、Kotlin/Native入門チュートリアルを参照してください。 新しいKotlin/Nativeプロジェクトを作成し、IntelliJ IDEAで開く方法について説明しています。

Kotlin/Nativeコンパイラは、KotlinのコードからmacOSおよびiOS用のフレームワークを生成できます。作成されたフレームワークには、Swift/Objective-Cで使用するために必要なすべての宣言とバイナリが含まれています。

まず、Kotlinライブラリを作成しましょう。

  1. src/nativeMain/kotlinディレクトリに、ライブラリの内容を含むlib.ktファイルを作成します。

    package example

    object Object {
    val field = "A"
    }

    interface Interface {
    fun iMember() {}
    }

    class Clazz : Interface {
    fun member(p: Int): ULong? = 42UL
    }

    fun forIntegers(b: Byte, s: UShort, i: Int, l: ULong?) { }
    fun forFloats(f: Float, d: Double?) { }

    fun strings(str: String?) : String {
    return "That is '$str' from C"
    }

    fun acceptFun(f: (String) `->` String?) = f("Kotlin/Native rocks!")
    fun supplyFun() : (String) `->` String? = { "$it is cool!" }
  2. build.gradle(.kts) Gradleビルドファイルを以下のように更新します。

    plugins {
    kotlin("multiplatform") version "2.1.20"
    }

    repositories {
    mavenCentral()
    }

    kotlin {
    iosArm64("native") {
    binaries {
    framework {
    baseName = "Demo"
    }
    }
    }
    }

    tasks.wrapper {
    gradleVersion = "8.10"
    distributionType = Wrapper.DistributionType.ALL
    }

    binaries {}ブロックは、動的ライブラリまたは共有ライブラリを生成するようにプロジェクトを構成します。

    Kotlin/Nativeは、iOS用のiosArm64iosX64iosSimulatorArm64ターゲット、およびmacOS用のmacosX64およびmacosArm64ターゲットをサポートしています。そのため、iosArm64()を、ターゲットプラットフォームに対応するGradle関数に置き換えることができます。

    ターゲットプラットフォーム/デバイスGradle関数
    macOS x86_64macosX64()
    macOS ARM64macosArm64()
    iOS ARM64iosArm64()
    iOS Simulator (x86_64)iosX64()
    iOS Simulator (ARM64)iosSimulatorArm64()

    サポートされている他のAppleターゲットについては、Kotlin/Nativeターゲットのサポートを参照してください。

  3. IDEでlinkDebugFrameworkNative Gradleタスクを実行するか、ターミナルで次のコンソールコマンドを使用して、フレームワークをビルドします。

    ./gradlew linkDebugFrameworkNative

ビルドにより、フレームワークがbuild/bin/native/debugFrameworkディレクトリに生成されます。

ヒント

linkNative Gradleタスクを使用して、フレームワークのdebugバリアントとreleaseバリアントの両方を生成することもできます。

生成されたフレームワークヘッダー

各フレームワークバリアントには、ヘッダーファイルが含まれています。ヘッダーは、ターゲットプラットフォームに依存しません。ヘッダーファイルには、Kotlinコードの定義と、いくつかのKotlin全体の宣言が含まれています。中身を見てみましょう。

Kotlin/Nativeランタイム宣言

build/bin/native/debugFramework/Demo.framework/Headersディレクトリで、Demo.hヘッダーファイルを開きます。Kotlinランタイム宣言を見てください。

NS_ASSUME_NONNULL_BEGIN
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-warning-option"
#pragma clang diagnostic ignored "-Wincompatible-property-type"
#pragma clang diagnostic ignored "-Wnullability"

#pragma push_macro("_Nullable_result")
#if !__has_feature(nullability_nullable_result)
#undef _Nullable_result
#define _Nullable_result _Nullable
#endif

__attribute__((swift_name("KotlinBase")))
@interface DemoBase : NSObject
- (instancetype)init __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
+ (void)initialize __attribute__((objc_requires_super));
@end

@interface DemoBase (DemoBaseCopying) <NSCopying>
@end

__attribute__((swift_name("KotlinMutableSet")))
@interface DemoMutableSet<ObjectType> : NSMutableSet<ObjectType>
@end

__attribute__((swift_name("KotlinMutableDictionary")))
@interface DemoMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType>
@end

@interface NSError (NSErrorDemoKotlinException)
@property (readonly) id _Nullable kotlinException;
@end

Kotlinのクラスは、Swift/Objective-CでKotlinBase基底クラスを持ち、そこでNSObjectクラスを拡張します。 コレクションと例外のラッパーもあります。ほとんどのコレクション型は、Swift/Objective-Cの同様のコレクション型にマッピングされます。

KotlinSwiftObjective-C
ListArrayNSArray
MutableListNSMutableArrayNSMutableArray
SetSetNSSet
MutableSetNSMutableSetNSMutableSet
MapDictionaryNSDictionary
MutableMapNSMutableDictionaryNSMutableDictionary

Kotlinの数値とNSNumber

Demo.hファイルの次の部分には、Kotlin/Nativeの数値型とNSNumberの間の型マッピングが含まれています。ベースクラスは、Objective-CではDemoNumber、SwiftではKotlinNumberと呼ばれます。これはNSNumberを拡張します。

Kotlinの数値型ごとに、対応する定義済みの子クラスがあります。

KotlinSwiftObjective-CSimple type
-KotlinNumber<Package>Number-
ByteKotlinByte<Package>Bytechar
UByteKotlinUByte<Package>UByteunsigned char
ShortKotlinShort<Package>Shortshort
UShortKotlinUShort<Package>UShortunsigned short
IntKotlinInt<Package>Intint
UIntKotlinUInt<Package>UIntunsigned int
LongKotlinLong<Package>Longlong long
ULongKotlinULong<Package>ULongunsigned long long
FloatKotlinFloat<Package>Floatfloat
DoubleKotlinDouble<Package>Doubledouble
BooleanKotlinBoolean<Package>BooleanBOOL/Bool

すべての数値型には、対応する単純型から新しいインスタンスを作成するクラスメソッドがあります。また、単純な値を抽出して戻すインスタンスメソッドもあります。概略的に、このような宣言はすべて次のようになります。

__attribute__((swift_name("Kotlin__TYPE__")))
@interface Demo__TYPE__ : DemoNumber
- (instancetype)initWith__TYPE__:(__CTYPE__)value;
+ (instancetype)numberWith__TYPE__:(__CTYPE__)value;
@end;

ここで、__TYPE__は単純な型名の1つであり、__CTYPE__は対応するObjective-C型です。 たとえば、initWithChar(char)です。

これらの型は、ボックス化されたKotlinの数値型をSwift/Objective-Cにマッピングするために使用されます。 Swiftでは、コンストラクタを呼び出してインスタンスを作成できます。たとえば、KotlinLong(value: 42)です。

Kotlinからのクラスとオブジェクト

classobjectがSwift/Objective-Cにどのようにマッピングされるかを見てみましょう。生成されたDemo.hファイルには、ClassInterface、およびObjectの正確な定義が含まれています。

__attribute__((swift_name("Interface")))
@protocol DemoInterface
@required
- (void)iMember __attribute__((swift_name("iMember()")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Clazz")))
@interface DemoClazz : DemoBase <DemoInterface>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (DemoULong * _Nullable)memberP:(int32_t)p __attribute__((swift_name("member(p:)")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Object")))
@interface DemoObject : DemoBase
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
+ (instancetype)object __attribute__((swift_name("init()")));
@property (class, readonly, getter=shared) DemoObject *shared __attribute__((swift_name("shared")));
@property (readonly) NSString *field __attribute__((swift_name("field")));
@end

このコードのObjective-C属性は、SwiftとObjective-Cの両方の言語からフレームワークを使用するのに役立ちます。DemoInterfaceDemoClazz、およびDemoObjectは、それぞれInterfaceClazz、およびObjectに対して作成されます。

Interface@protocolに変換され、classobjectの両方が@interfaceとして表されます。 Demoプレフィックスは、フレームワーク名に由来します。nullableの戻り値の型ULong?は、Objective-CではDemoULongに変換されます。

Kotlinからのグローバル宣言

Kotlinからのすべてのグローバル関数は、Objective-CではDemoLibKtに、SwiftではLibKtに変換されます。 ここで、Demokotlinc-native-outputパラメータで設定されたフレームワーク名です。

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LibKt")))
@interface DemoLibKt : DemoBase
+ (NSString * _Nullable)acceptFunF:(NSString * _Nullable (^)(NSString *))f __attribute__((swift_name("acceptFun(f:)")));
+ (void)forFloatsF:(float)f d:(DemoDouble * _Nullable)d __attribute__((swift_name("forFloats(f:d:)")));
+ (void)forIntegersB:(int8_t)b s:(uint16_t)s i:(int32_t)i l:(DemoULong * _Nullable)l __attribute__((swift_name("forIntegers(b:s:i:l:)")));
+ (NSString *)stringsStr:(NSString * _Nullable)str __attribute__((swift_name("strings(str:)")));
+ (NSString * _Nullable (^)(NSString *))supplyFun __attribute__((swift_name("supplyFun()")));
@end

KotlinのStringとObjective-CのNSString*は透過的にマッピングされます。同様に、KotlinのUnit型はvoidにマッピングされます。 プリミティブ型は直接マッピングされます。nullableでないプリミティブ型は透過的にマッピングされます。 nullableのプリミティブ型は、に示すように、Kotlin<TYPE>*型にマッピングされます。 高階関数acceptFunFsupplyFunの両方が含まれており、Objective-Cブロックを受け入れます。

型マッピングの詳細については、Swift/Objective-Cとの相互運用性を参照してください。

ガベージコレクションと参照カウント

SwiftとObjective-Cは、自動参照カウント(ARC)を使用します。Kotlin/Nativeには、独自のガベージコレクタがあり、 これはObjective-C/Swift ARCと統合されています。

未使用のKotlinオブジェクトは自動的に削除されます。SwiftまたはObjective-CからKotlin/Nativeインスタンスのライフタイムを制御するために追加の手順を実行する必要はありません。

Objective-Cからコードを使用する

Objective-Cからフレームワークを呼び出してみましょう。フレームワークディレクトリに、次のコードを含むmain.mファイルを作成します。

#import <Foundation/Foundation.h>
#import <Demo/Demo.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
[DemoObject.shared field];

DemoClazz* clazz = [[ DemoClazz alloc] init];
[clazz memberP:42];

[DemoLibKt forIntegersB:1 s:1 i:3 l:[DemoULong numberWithUnsignedLongLong:4]];
[DemoLibKt forIntegersB:1 s:1 i:3 l:nil];

[DemoLibKt forFloatsF:2.71 d:[DemoDouble numberWithDouble:2.71]];
[DemoLibKt forFloatsF:2.71 d:nil];

NSString* ret = [DemoLibKt acceptFunF:^NSString * _Nullable(NSString * it) {
return [it stringByAppendingString:@" Kotlin is fun"];
}];

NSLog(@"%@", ret);
return 0;
}
}

ここでは、KotlinクラスをObjective-Cコードから直接呼び出します。Kotlinオブジェクトは、<object name>.sharedクラスプロパティを使用します。これにより、オブジェクトの唯一のインスタンスを取得し、そのオブジェクトメソッドを呼び出すことができます。

広範囲にわたるパターンは、Clazzクラスのインスタンスを作成するために使用されます。Objective-Cで[[ DemoClazz alloc] init]を呼び出します。パラメータのないコンストラクタには、[DemoClazz new]も使用できます。

Kotlinソースからのグローバル宣言は、Objective-CのDemoLibKtクラスの下にスコープされます。 すべてのKotlin関数は、そのクラスのクラスメソッドに変換されます。

strings関数はObjective-CでDemoLibKt.stringsStr関数に変換されるため、NSStringを直接渡すことができます。 戻り値もNSStringとして表示されます。

Swiftからコードを使用する

生成したフレームワークには、Swiftでの使用を容易にするヘルパー属性があります。前のObjective-Cの例をSwiftに変換してみましょう。

フレームワークディレクトリに、次のコードを含むmain.swiftファイルを作成します。

import Foundation
import Demo

let kotlinObject = Object.shared

let field = Object.shared.field

let clazz = Clazz()
clazz.member(p: 42)

LibKt.forIntegers(b: 1, s: 2, i: 3, l: 4)
LibKt.forFloats(f: 2.71, d: nil)

let ret = LibKt.acceptFun { "\($0) Kotlin is fun" }
if (ret != nil) {
print(ret!)
}

元のKotlinコードとそのSwiftバージョンには、いくつかの小さな違いがあります。Kotlinでは、任意のオブジェクト宣言には1つのインスタンスしかありません。Object.shared構文は、この単一のインスタンスにアクセスするために使用されます。

Kotlinの関数名とプロパティ名はそのまま変換されます。KotlinのStringはSwiftのStringに変換されます。SwiftはNSNumber*のボクシングも非表示にします。SwiftクロージャをKotlinに渡し、SwiftからKotlinラムダ関数を呼び出すこともできます。

型マッピングの詳細については、Swift/Objective-Cとの相互運用性を参照してください。

フレームワークをiOSプロジェクトに接続する

これで、生成されたフレームワークを依存関係としてiOSプロジェクトに接続できます。設定してプロセスを自動化する方法は複数あります。最適な方法を選択してください。

Choose iOS integration method

次のステップ