Bollywood HungamaHanuman Ansh producer reveals film's EXACT budget for the FIRST time; also reveals that Haldiram invested Rs. 1.25 cr in the SLEEPER blockbusterThe Jerusalem PostIDF kills three Hamas terrorists, including commander, during latests Gaza strikesPunchOgun Ohanaeze Ndi Igbo endorses Tinubu, Yayi for 2027 electionsCNN TürkEge’de NAVTEX Savaşı! Türkiye ve Yunanistan arasında neler oluyor?RTP DesportoSporting de Braga e Gil Vicente acertam calendário na I Liga de futebol한겨레Young Korean parents get 1.5 fewer hours of leisure a day than unmarried peers, survey findsEgypt IndependentHow can Egypt intervene to support Saudi Arabia? An expert explainsDaily MaverickAll clear given after bomb threat diverts Austrian Airlines flight from Israel to Bulgaria20 MinutenKylie Jenner stolziert mit 4400 Diamanten über den LaufstegUOLHomem detido por panelaço em Maceió questiona PM: 'Por que só o porteiro?'South China Morning PostChina’s export boom creates a difficult bargain for Southeast AsiaVariety‘Barry Loser’ Animated Series, Based on Bestselling U.K. Children’s Books, Finds Italian Partners Ahead of Rome’s MIA Market (EXCLUSIVE)
The Daily Newsstand · Free, Always
Monday, October 19, 2026

Swift на Android: путь к сингулярности. От анонса SwiftJava до полноценного приложения. Часть 1

Translate

Всем привет! С вами Анна Жаркова, руководитель практики мобильной разработки ГК Юзтех, автор книги «Kotlin Multiplatform на практике» и человек, который уже который год следит за тем, как две мобильные экосистемы (Apple и Android) медленно, но верно ползут друг к другу. Сегодня мы с вами обсудим наработки от Apple. Какую технологию они представили еще в 2025 году, что изменилось со времени, как Apple выпустили официальный релиз Swift SDK для Android. А также мы посмотрим, как написать настоящее приложение, где вся бизнес-логика живёт на Swift, а UI на Compose.

Но давайте по порядку.

Июнь 2025 года, WWDC. По традиции Apple проводят большую презентацию и, среди прочего, громко анонсируют инструмент SwiftJava, который был призван не столько навести мост между двумя языками, сколько сделать портирование с одной технологии на другую абсолютно лёгким и бесшовным. С помпой рассказали, как портировали пару Java-библиотек под Swift и как хорошо это работает.

В пакет решений вошли 2 библиотеки:

  • JavaKit — набор Swift-макросов и инструментов, которые генерируют исходники Swift для вызова Java-кода. То есть берём Java-библиотеку и подключаем её к коду на Swift.

  • SwiftKit — обратная библиотека, которая работает под Java и позволяет вызывать Swift-типы из Java. То есть берём Swift-библиотеку и используем её на стороне Java (и, как следствие, на Android).

По заверению Apple, всё это позволяет буквально за пару действий превратить Java-библиотеку в библиотеку на Swift, и обратно.

Ниже представлен пример, что получается при генерации Swift из Java класса:

package com.example;

public class Greeting {
    public static String greet(String name) {
        return "Hello, " + name + "!";
    }
}

Кстати, для демонстрации инженеры Apple брали самый простой код.

Собственно результат работы Java-to-Swift генератора:

// Auto-generated by Java-to-Swift wrapper generator.
import CSwiftJavaJNI
import SwiftJava

@JavaClass("com.example.Greeting")
open class Greeting: JavaObject {
  @JavaMethod
  @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
}
extension JavaClass<Greeting> {
  @JavaStaticMethod
  public func greet(_ arg0: String) -> String
}

Java-класс com.example.Greeting будет теперь доступен из Swift как обычный Swift-тип, и работать с ним можно так же: инициализируем его инстанс, вызываем методы, а под капотом всё уходит в JNI. Это и есть та самая «магия WWDC».

Впечатляет. Но остается вопрос, а для чего все это нужно?

Официальная версия компании Apple : миграция приложения Password Monitoring (мониторинг утечек паролей)с Java на Swift. Независимо от статистики использования данной утилиты выглядит несколько неубедительно. В отличие от версии распространения влияния Swift за пределы экосистемы Apple и превращения из языка мультиплатформы в кроссплатформу.

Для чего же нужна данная технология:

  • создание общего код без другого кроссплатформенного слоя;

  • развитие сближения Kotlin и Swift;

  • интеграция Swift-библиотек в Java/Android;

  • интеграция Java-библиотек в Swift (как вариант).

Прежде чем браться за новый инструмент, давайте вспомним, как вообще связываются языки в существующих кроссплатформенных решениях. Без этого будет сложно понять, где Apple пошли своим путём, а где повторили чужие ошибки. Сделаем мы это, конечно же, на примере Kotlin Multiplatform.

Как мы помним, общий модуль кода не монолитный: он состоит из общей части и платформенных частей, для каждой из которых используется своя версия Kotlin (Kotlin/JVM для Java и Android и Kotlin/Native для iOS и десктопов). Для корректной связки кода под капотом, есть механизм expect/actual, который позволяет переопределить реализацию конструкций кода под разные таргеты.

Если говорить про Kotlin/JVM, всё просто: Kotlin вышел из Java, это JVM-язык, и интероп Kotlin↔Java практически полный. А вот с Kotlin/Native интереснее. Связь между Kotlin/Native и Swift идёт через Objective-C: ваш код преобразуется в заголовочные файлы Objective-C, которые затем связываются со Swift через специальные bridge

Kotlin взаимодействует с C через JNI. Кто работал с JNI, знает: это способ подключить низкоуровневую логику, то есть готовые библиотеки на C/C++ (криптография, OpenGL и т.д.). Для интеграции в Android приложения используется Android NDK. Ниже представлена структура модуля приложения с подключением JNI кода:

my-app/
├── cpp/
│   └── native-lib.cpp        # наша логика на C++
├── CMakeLists.txt            # где искать cpp-шники
└── java/com/example/app/
    └── NativeBridge.java     # class с native-методами
public class NativeBridge {
    static {
        System.loadLibrary("native-lib");
    }

    public native String stringFromJNI();
}
val bridge = NativeBridge()
println(bridge.stringFromJNI())

Со стороны C/C++ для каждого native-метода нужна декларация имен типов вида Java_пакет_класс_метод. Как мы понимаем, руками такое писать будет сложно и даже неприятно:

// nativeutils.h
#include <jni.h>

#ifndef _Included_com_example_jni_NativeUtils
#define _Included_com_example_jni_NativeUtils

#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     com_example_jni_NativeUtils
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_jni_NativeUtils_sayHello
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Apple решили взять JNI-подход на вооружение. Их официальный рекламный лозунг гласит: «Generates hard-to-get-right code», то есть генерирует код, который сложно написать правильно это не преувеличение: корректный JNI-код действительно сложно писать вручную. Вопрос в другом: что в итоге генерируется и насколько это пригодно к использованию. Посмотрим в репозиторий проекта: github.com/swiftlang/swift-java.

Инструмент активно развивается. Баги и проблемы, а также тикеты на доработку и поддержку, постоянно заводятся, правятся, заливаются и сменяются новыми. На данный момент SwiftJava довольно рискованно использованном в настоящей прод-разработке.

Внутри SwiftJava есть утилита jextract, она же основной рабочий инструмент, которая и запускает генерацию hard-to-get-right code.

У него два направления работы:

  • JavaKit (Java → Swift): генерация Swift-обёртки из Java-кода, чтобы вызывать Java из Swift.

  • SwiftKit (Swift → Java): генерация Java-биндингов из Swift, чтобы вызывать Swift из Java и из Android.

Также есть два режима поддержки взаимодействия Java с нативным кодом:

swift-java jextract --mode=ffm   (режим по умолчанию)
swift-java jextract --mode=jni
  • FFM (Foreign Function & Memory API) требует Swift 6.2+ и JDK 25 (по факту работает начиная с JDK 22).

  • JNI требует Swift 6.2+ и JDK 7+. Это старый добрый JNI.

В общих чертах алгоритм выглядит так: вы запускаете jextract по вашему Swift-коду, и он генерирует Java-биндинги — Java-классы, которые зеркалируют Swift-типы и объявляют native-методы. На стороне Swift генерируются thunks: функции с атрибутом @_cdecl, которые становятся точками входа для JNI. Дополнительно создаются linker export lists: скрипты, которые экспортируют из итоговой shared library только нужные JNI-символы.

# ручной режим: указываем режим API и путь к исходникам
swift run jextract --mode jni path/to/Sources/MyLibrary

Посмотрим на небольшом примере. Возьмем простой Swift-класс:

public class MySwiftClass {
  public let x: Int64
  public init(x: Int64) {
    self.x = x
  }

  public func printMe() {
    print("\(self.x)");
  }
}

После запуска jextract получаем Java-класс, который реализует JNISwiftInstance: публичные методы представляют собой тонкие обёртки, а внизу объявлены native-методы $init, $getX, $printMe, которые и есть точки входа в среду исполнения Swift:

public final class MySwiftClass implements JNISwiftInstance {
  public static  MySwiftClass init(long x, long y, SwiftArena swiftArena$) {
    return MySwiftClass.wrapMemoryAddressUnsafe(MySwiftClass.$init(x, y), swiftArena$);
  }

  public  long getX() {
    return MySwiftClass.$getX(this.$memoryAddress());
  }

  public  void printMe() {
    MySwiftClass.$printMe(this.$memoryAddress());
  }

  private static native long $init(long x, long y);
  private static native long $getX(long self);
  private static native void $printMe(long self);
}

Возвращаясь к режиму взаимодействия с нативным кодом, зачем нужны JNI и FFM? FFM представляет собой новый стандартJava Foreign Function & Memory API (JEP 454), который пришёл на смену JNI и позволяет вызывать нативный код без JNI-прослойки и без классического копирования данных.

JNI

FFM

Java

11+ (для jextract: JDK 7+)

22+ (заявлено 25)

Android

Совместим

Не поддерживается

Копирование данных

Больше копий

Лучше поддержка нативной памяти

Набор возможностей

Шире

«Базовый» набор

Но если мы работаем с Android, то нам FFM не подойдет из-за отсутствия поддержки версии SDK. Будем работать с JNI.

Также стоит упомянуть, что как и в случае взаимодействия Swift и Kotlin, при конвертации Swift <-> Java не все конструкции языка и SDK поддерживаются. Например, исключения, коллекции, перечисления (enum), дженерики, структурированная многопоточность Swift (async/await работал только с JNI, акторы не поддерживались). Сейчас большинство этих проблем решены. Например, async/await конвертируется в CompletableFuture, а для управления памятью используется механизм SwiftArena. Но об этом позже.

Пришло время посмотреть на реальной практике, а заодно и обсудить Swift SDK для Android. Кстати, к весне 2026 выпустили уже стабильный официальный релиз, работающий с Swift, начиная с версии 6.3.

Обратите внимание, несмотря на наличие официального релиза, данная технология не рекомендуется для прод-разработки. Также вы не напишете на ней приложение полностью.

Какие сценарии и слои сейчас поддерживаются:

  • бизнес-логика;

  • модели данных;

  • сетевой слой и хранение;

  • архитектурные компоненты;

  • фоновая обработка.

Та же история, что с KMP года 3 назад, но с оговорками.

UI рекомендуется реализовывать нативно, отдельно на Android и iOS. Также парадигма «write once, run everywhere» сейчас не работает. Только shared logic без кроссплатформенного UI-слоя.

Попробуем реализовать на Swift SDK для Android простое приложение новостей newsapi.org.

Наше приложение будет состоять из следующих модулей:

  • :app — наше Android-приложение, мы сделаем нативнуюNewsViewModel, а также UI на Compose;

  • :news-shared (Swift PM, таргет :newslib) — общий модуль, в котором будет лежать логика по работе с сетью, модели данных, бизнес-логика.

Исходники примера целиком: gitverse.ru/azharkova/swift-droid-news-sample.

Начнем с установки утилит и библиотек:

swiftly — менеджер тулчейнов Swift от самой Apple. Ставим нужную версию Swift (сейчас 6.4.1):

% swiftly install latest
Fetching the latest stable Swift release...
Installing Swift 6.4.1

Installing package in user home directory...
Swift 6.4.1 is installed successfully!
% swiftly use latest
The global default toolchain has been set to `Swift 6.4.1`

% swift --version
Apple Swift version 6.4.1 (swift-6.4.1-RELEASE)
Target: arm64-apple-macosx26.0

Android Swift SDK — сам SDK для кросс-компиляции под Android

% swift sdk install https://download.swift.org/swift-6.4.1-release/android-sdk/swift-6.4.1-RELEASE/swift-6.4.1-RELEASE_android.artifactbundle.tar.gz --checksum a356b79f8f1daa6ae3a66f4c556966ca20c0208b3659805cf30a83a0a31d6772

Android NDK: если NDK уже установлен, достаточно прописать переменную окружения ANDROID_NDK_HOME.

% curl -fSL -o ndk.zip https://dl.google.com/android/repository/android-ndk-r30-$(uname -s).zip
% unzip -qo ndk.zip
% export ANDROID_NDK_HOME=$PWD/android-ndk-r30

Готово.

Создаем обычное Android приложение и реализуем структуру подключения модуля.

Модуль news-shared отвечает за общий код. В нем у нас будет одновременно Gradle-модулем и описание Swift Package. В src/debug лежат сгенерированные библиотечные файлы jniLibs (готовые .so), в src/main/java находятся bridge-компоненты на Kotlin со связывающими конструкциями, в src/main/swift лежат Swift-исходники с Package.swift.

Пропишем структуру Package.Swift. Нам необходимо заявить наш модуль динамической библиотекой. А в качестве зависимостей прописать swift-java нужной версии. Добавить плагин JExtractSwiftPlugin из пакета swift-java для генерации Java-биндингов при сборке., прописать версию Swift, добавить флаги многопоточности. Обратите внимание, файл конфигурации мы исключаем из SPM.

Setup: Package.swift

let package = Package(
    name: "newslib",
    products: [
        .library(name: "newslib", type: .dynamic, targets: ["NewsShared"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-java", from: "0.2.0"),
        .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.33.1"),
    ],
    targets: [
        .target(
            name: "NewsShared",
            dependencies: [
                .product(
                    name: "AsyncHTTPClient",
                    package: "async-http-client",
                    condition: .when(platforms: [.android])
                ),
                .product(name: "SwiftJava", package: "swift-java"),
            ],
            exclude: ["swift-java.config"],
            swiftSettings: [
                .swiftLanguageMode(.v6),
                .unsafeFlags(["-Xfrontend", "-strict-concurrency=minimal"]),
            ],
            plugins: [
                .plugin(name: "JExtractSwiftPlugin", package: "swift-java"),
            ]
        ),
    ]
)

Также мы будем использовать AsyncHTTPClient для платформы Android. Это специальная библиотека для решений SwiftJava для работы с сетью.

Теперь займемся скриптом build.gradle.kts

Setup: build.gradle.kts

В Gradle-модуле news-shared указываем путь к скрипту сборки Swift-части и зависимость от SwiftKit runtime:

// news-shared/build.gradle.kts (фрагмент)
apply(from = "../swift-android.gradle.kts")

dependencies {
    implementation(libs.androidx.core.ktx)
    api(libs.swiftkit.core) // org.swift.swiftkit:swiftkit-core
}

swiftkit-core представляет собой runtime-библиотеку org.swift.swiftkit:swiftkit-core, в которой находятся SwiftArena, JNISwiftInstance и вся инфраструктура управления памятью.

Основной код по настройке приложения вы можете найти в репозитории с демо-проектом.

Setup: swift-java.config

Конфигурация генерации лежит в корне Swift-таргета и выглядит так:

{
  "javaPackage": "ru.azharkova.sampleapp.newslib.swift",
  "nativeLibraryName": "newslib",
  "mode": "jni",
  "asyncFuncMode": "legacyFuture"
}

Здесь важны все параметры: имя пакета для сгенерированных Java-классов, имя нативной библиотеки, режим jni (помним: FFM на Android не работает) и режим асинхронных функций legacyFuture включает мост CompletableFuture < - > async/await.

Приступим к реализации сетевого слоя. На iOS/macOS мы бы использовали URLSession, но FoundationNetworking на Android недоступен. Совместно с Android NDK мы можем использовать SwiftNIO. Для этого подключим библиотеку AsyncHTTPClient. Для запросов мы будем использовать конструкции специального сетевого транспорта:

private static func makeTransport() -> some ClientTransport {
    #if os(Android)
    return AsyncHTTPClientTransport(configuration: .init())
    #else
    return URLSessionTransport()
    #endif
}

#if os(Android) срабатывает для Android-таргета, #if canImport(Foundation) срабатывает для iOS/macOS. С помощью условной компиляции один и тот же Swift-код собирается и под Apple-платформы, и под Android.

Такую фабрику транспорта подключаем в обычный сетевой клиент, который пишем на Swift:

final class NetworkClient {
    private let transport: any NetworkTransport

    init(transport: any NetworkTransport = makeTransport())

    func execute<Request: NetworkRequest>(_ request: Request) async throws -> NetworkResponse
}

Но работать с сырыми данными будет неудобно, некрасиво. Нам потребуются дополнительные компоненты - различные декодеры и мапперы. Да и об архитектуре надо не забывать. Соединим это все в NewsApiService:

final class NewsApiService: NewsServicing {
    private let apiKey: String
    private let client: NetworkClient
    private let responseDecoder: NetworkResponseDecoder
    private let articleMapper: NewsArticleMapper
    private let errorMapper: NewsServiceErrorMapper

    func fetchTopHeadlines(country: String) async throws -> [NewsArticle] {
        //...
        do {
            let response = try await client.execute(TopHeadlinesRequest(...))
            let payload = try await responseDecoder.decode(
                NewsAPIResponse.self, from: response.data)

            return payload.articles.map(articleMapper.map)
        } catch {
            throw await errorMapper.map(error)
        }
    }
}

Выбор за тем, как будут называться компоненты в вашем коде, как всегда, за вами.

Теперь нам нужна публичная точка вызова нашего кода со стороны Java/ Android. Этот же компонент будет прокси для нашего кода. И вот его мы в экспортированном виде будем вызывать со стороны нативного приложения:

public final class NewsService {
    private let service: NewsApiService

    public init(apiKey: String) throws {
        self.service = try NewsApiService(apiKey: apiKey)
    }

    public func fetchTopHeadlines(country: String) async throws -> [NewsArticle] {
        try await service.fetchTopHeadlines(country: country)
    }
}

Сгенерированный jextract-класс NewsService виден из Kotlin как обычный класс. Вызывать и инициализовать мы его будем не напрямую в ViewModel, а сделаем еще один прокси (Bridge). Bridge на Kotlin хранит инстанс сгенерированного компонента из Swift-класса, а также вызов его метода через suspend:

class NewsServiceBridge(apiKey: String) {
    private val swiftNewsService: NewsService = NewsService.init(apiKey)

    suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
        val articles = swiftNewsService.fetchTopHeadlines(country).awaitSwift()
        return articles.map(::toUiArticle)
    }
}

Обратите внимание на .awaitSwift(): это наше Kotlin-расширение для CompletableFuture.

Подключим все в ViewModel. На стороне Android используем компонент из пакета Jetpack. В него мы добавим вызов сервиса через бридж:

class NewsViewModel(
    private val apiKey: String = BuildConfig.NEWS_API_KEY,
    private val country: String = BuildConfig.NEWS_COUNTRY
) : ViewModel() {
    private val newsServiceBridge: NewsServiceBridge?
    //...

    fun refresh() {

        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)

            val session = newsServiceBridge

            try {
                val articles = session.fetchTopHeadlines(country = country)
                _uiState.value = NewsUiState(articles = articles)
            } catch (throwable: Throwable) {...}
        }
    }
}

Однако, в такой реализации оно не будет работать. Даже не скомпилируется. Дело в том, что нам нужно прокинуть связку для управления памятью Swift в наш Kotlin-код. Как это сделать, посмотрим ниже.

Кроссплатформенные решения всегда требуют более внимательного отношения к управлению памятью, потому что кода генерируется больше, генерируются промежуточные стабы и прокси. Объекты должны удерживаться и освобождаться в Swift-куче, при этом оставаясь видимыми для Kotlin/Java GC. Это требует дополнительной работы и синхронизации двух менеджеров памяти. Также нужно больше внимания к переключению контекста как внутри платформы, так и между ними.

JVM управляет памятью через сборщик мусора (garbage collection), а Swift использует автоматический подсчёт ссылок (ARC). Эти две модели ничего не знают друг о друге. Java GC не может видеть Swift-типы и Swift-кучу: когда jextract передаёт Java-объекту Swift-инстанс, на самом деле передаётся opaque-указатель (адрес в памяти). По этому адресу объект находится в Swift-куче, и GC его не видит вообще.

Для решения этой проблемы используется механизм SwiftArena, в которой регистрируются адреса Swift-объектов, вызываемых из Java:

public interface SwiftArena {
    void register(SwiftInstance instance);

    static ClosableSwiftArena ofConfined() {
        return new ConfinedSwiftMemorySession();
    }

    static SwiftArena ofAuto() {
        ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread");
        return new AutoSwiftMemorySession(cleanerThreadFactory);
    }
}

Как вы видите, также используется привязка к определенным потокам.

Есть несколько видов специальных арен:

Интерфейс / класс

Ответственность

Жизненный цикл

ClosableSwiftArena

Расширяет SwiftArena и AutoCloseable — для использования с try-with-resources

Явный (close())

ConfinedSwiftMemorySession

ClosableSwiftArena, ведёт список ресурсов

Явный (close())

AutoSwiftMemorySession

Чистку запускает SwiftCleaner через GC

Неявный (GC-driven)

AllocatingSwiftArena

Реализация FFM: связывает время жизни Swift-объекта с MemorySegment

Явный или GC

Для корректной работы нам потребуется арена с явным механизмом закрытия и вызова очистки и арена с автоочисткой. Арена AutoSwiftMemorySession при регистрации инстанса объекта создает и хранит специальный метод очистки, ссылка на это действие будет позже передана компоненту SwiftCleaner:

final class AutoSwiftMemorySession implements SwiftArena {
    private final SwiftCleaner swiftCleaner;

    public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) {
        this.swiftCleaner = SwiftCleaner.create(cleanerThreadFactory);
    }

    @Override
    public void register(SwiftInstance instance) {
        Objects.requireNonNull(instance, "value");

        // Гарантируем, что instance не захватится во время очистки,
        // поэтому можем игнорировать предупреждение ниже.
        var cleanupAction = instance.$createCleanup();
        swiftCleaner.register(instance, cleanupAction);
    }
}

Возвращаемся к нашему коду. Посмотрим на сгенерированный Java-код для нашего NewsService. Несмотря на то, что у нас этот параметр указан не был, инициализатор Swift типа явно требует SwiftArena на Java:

// Инстанс Swift
public final class NewsService {
    public init(apiKey: String) {}
}

// JNISwiftInstance
public static NewsService init(java.lang.String apiKey, SwiftArena swiftArena) throws Exception {
    return NewsService.wrapMemoryAddressUnsafe(NewsService.$init(apiKey), swiftArena);
}

private NewsService(long selfPointer, SwiftArena swiftArena) {
    SwiftObjects.requireNonZero(selfPointer, "selfPointer");
    this.selfPointer = selfPointer;

    // Only register once we have fully initalized the object
    // since this will need the object pointer.
    swiftArena.register(this);
}

public static NewsService wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) {
    return new NewsService(selfPointer, swiftArena);
}

public static NewsService wrapMemoryAddressUnsafe(long selfPointer) {
    return new NewsService(selfPointer, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);
}

Поэтому нам нужно будет его передать.

Арена регистрирует объект только после полной инициализации, поскольку ей нужен валидный указатель. Вызов метода у обёртки представляет собой обращение по указателю $memoryAddress() к настоящему Swift-инстансу:

Передаваемый инстанс арены должен быть проинициализирован явно: без настоящей привязки SwiftArena поиск объекта по указателю может привести к аварийному завершению.

Обратную сторону жизненного цикла представляет функция уничтожения. Сгенерированный $createDestroyFunction возвращает Runnable, который вызывает SwiftObjects.destroy по сохранённому указателю и адресу метаданных типа:

@Override
public Runnable $createDestroyFunction() {
    long self$ = this.$memoryAddress();
    long selfType$ = this.$typeMetadataAddress();
    if (CallTraces.TRACE_DOWNCALLS) {
        CallTraces.traceDowncall("NewsService.$createDestroyFunction",
            "this", this,
            "self", self$);
    }
    return new Runnable() {
        @Override
        public void run() {
            if (CallTraces.TRACE_DOWNCALLS) {
                CallTraces.traceDowncall("NewsService.$destroy", "self", self$);
            }
            SwiftObjects.destroy(self$, selfType$);
        }
    };
}

При этом объект остаётся видимым для GC. Wrapper в Java-куче собирается GC как обычно, а вот Swift-память освобождается только тогда, когда арена (или cleaner) вызовет destroy-функцию.

Хорошо, но мы забыли указать, как мы эту арену создаем, откуда берем и к чему она привязана. Конечно же, к жизненному циклу ViewModel. Создаём ClosableSwiftArena через SwiftArena.ofConfined(), передаём её в bridge, а в onCleared() закрываем и вызываем очистку:

class NewsViewModel(
    private val swiftArena: ClosableSwiftArena = SwiftArena.ofConfined()
) : ViewModel() {

    private val newsServiceBridge: NewsServiceBridge?

    init {
        var createdSession: NewsServiceBridge? = null
        try {
            createdSession = NewsServiceBridge(
                swiftArena = swiftArena
            )
        } catch (throwable: Throwable) {
            initializationError = throwable
        }

        newsServiceBridge = createdSession
    }

    override fun onCleared() {
        newsServiceBridge?.close()
        super.onCleared()
    }
}

А в bridge при этом реализует интерфейс AutoCloseable для корректного вызова очистки арены для финального освобождения ресурсов:

class NewsServiceBridge(
    private val swiftArena: ClosableSwiftArena
) : AutoCloseable {
    // Здесь хранится именно generated Swift instance, а не Kotlin service.
    private val swiftNewsService: NewsService = NewsService.init(swiftArena)

    suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
        val articles = swiftNewsService.fetchTopHeadlines(country, swiftArena).awaitSwift()
        return articles.map(::toUiArticle)
    }

    override fun close() {
        swiftArena.close()
    }
}

Итоговая цепочка: ViewModel уничтожается → onCleared() → bridge.close() → swiftArena.close() → destroy всех зарегистрированных Swift-объектов.

Управление памятью всегда связано с многопоточностью. JVM concurrency изначально несовместима со Swift concurrency. Но у нас есть решение. При этом мы можем выбрать, сделать все быстро и автоматически через конвертацию CompletableFuture <-> async/await или вручную через @_cdecl, подключение/отключение указателей к скоупу исполнения (как в графе для Kotlin/Native). Мы пойдем простым путем. Для поддержки CompletableFuture < - > async/await в конфигурационном файле добавим строку "asyncFuncMode": "legacyFuture". И на этом все. Сгенерированный метод будет выглядеть так:

public java.util.concurrent.Future<NewsArticle[]> fetchTopHeadlines(java.lang.String country,
SwiftArena swiftArena) {
    org.swift.swiftkit.core.SimpleCompletableFuture<long[]> future$ = new
org.swift.swiftkit.core.SimpleCompletableFuture<long[]>();
    NewsService.$fetchTopHeadlines(country, this.$memoryAddress(), future$);
    return future$.thenApply((futureResult$) -> {
      return Arrays.stream(futureResult$).mapToObj((pointer) -> {
        return NewsArticle.wrapMemoryAddressUnsafe(pointer, swiftArena);
      } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+JavaTranslation.swift:2024
      ).toArray(NewsArticle[]::new);
    } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+JavaTranslation.swift:2024
    );
}

Swift async-функция автоматически маппится в Future, а каждый элемент результата (указатель) оборачивается в Java-обёртку через wrapMemoryAddressUnsafe с той же ареной. Для сахара и удобства можно добавить расширение в Kotlin для вызова Future внутри корутины и корректной конвертации между ними:

private suspend fun <T> Future<T>.awaitSwift(): T = withContext(Dispatchers.IO) {
    get()
}

Добавляем вызов в наш bridge:

suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
    val articles = swiftNewsService.fetchTopHeadlines(country, swiftArena).awaitSwift()
    return articles.map(::toUiArticle)
}

Собираем Swift-модуль кросс-компиляцией под Android (swift build --arch aarch64-unknown-linux-android24), на выходе получаем .so, передаём на устройство через adb push, не забыв вручную положить рядом libc++_shared.so. После Gradle-сборки в build/generated/jniLibs/arm64-v8a оказывается следующий набор библиотек:

Тут и libnewslib.so / libnewsshared.so (наша логика), и среда исполнения Swift: libswiftCore.so, libswift_Concurrency.so, libFoundation.so, libdispatch.so, libSwiftJava.so и десяток других. Сгенерированного кода очень много, и держать его под контролем версий смысла нет.

Интересный нюанс: папка build модуля news-shared весит 992 МБ. Сама .so в приложении заметно скромнее, APK вырос примерно на 8 МБ, но процесс сборки тяжёлый и долгий.

Получаем Android-приложение, в котором лента новостей загружается Swift-кодом через AsyncHTTPClient, маппится в UI-модели и показывается в Compose:

Репозиторий сэмпла: gitverse.ru/azharkova/swift-droid-news-sample.

Когда же будет хорошо (приемлемо) это использовать:

  • Just for fun — хобби-проекты, эксперименты, участие в развитии технологии;

  • Расширение существующего iOS-приложения на Android — когда бизнес-логика уже написана на Swift и команда хочет переиспользовать её без переписывания на Kotlin.

Для всего остального подойдут более зрелые инструменты.

Что было бы полезно получить в дальнейшем:

  • Полная поддержка async/await для Android (сейчас реализуется через обходное решение на CompletableFuture);

  • Кросс-компиляция и поддержка тестирования;

  • Расширения для VSCode.

Публичная дорожная карта лежит на GitHub: github.com/orgs/swiftlang/projects/17.

Источники

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.