Как расширить общий модуль KMM с помощью кода C/C++

Как расширить общий модуль KMM с помощью кода C/C++

8 февраля 2023 г.

В этом руководстве показано, как использовать код C/C++ в общем модуле KMM. Вы узнаете, как внедрить простую реализацию SHA-256 на языке C в библиотеку KMM и использовать ее для целевых платформ iOS и Android.

<цитата>

Kotlin Multiplatform Mobile (KMM) – это SDK, предназначенный для упрощения разработки кроссплатформенных мобильных приложений. Вы можете использовать общий код между приложениями для iOS и Android и писать специфичный для платформы код только там, где это необходимо

Вы можете использовать код C и C++ как с Android, так и с iOS (Xcode поддерживает C, C++ и стандартные версии, а Android имеет Android NDK). KMM использует эту функцию и позволяет вам использовать код C и C++ в вашем общем модуле.

Это может быть полезно при разработке приложений; есть много библиотек, которые уже написаны на C/C++. Отличным примером этого метода является проект Zipline, который включает в себя движок QuickJs в коде KMM.

В этой статье мы не будем говорить о том, как это работает внутри, а вместо этого сосредоточимся на написании кода и настройке сборки Gradle.

:::подсказка Код этой статьи можно найти в репозитории

.

:::

Начните создавать библиотеку для iOS и Android

Мы не будем говорить о том, как создать библиотеку KMM, так как это очень хорошо объяснено в официальное руководство по Kotlin.

Возьмем в качестве примера код SHA-256, написанный на C, и скопируем его в нашу папку native/sha256. Пример кода для реализации SHA-256 был взят из этого репозитория.< /p>

Поскольку приложения Android и iOS поддерживают C-код по-разному, нам нужно использовать ожидаемый/фактический подход. .

Прежде всего, давайте настроим нашу общую часть:

/**
 * Encoder interface which can encode byte arrays to Sha256 format.
 */
interface Sha256Encoder {
    fun encode(src: ByteArray): ByteArray

    fun encodeToString(src: ByteArray): String {
        val encoded = encode(src)
        return buildString(encoded.size) {
            encoded.forEach { append(it.toUByte().toString(16).padStart(2, '0')) }
        }
    }
}

expect object Sha256Factory {
    /**
     * Creates a new instance of [Sha256Encoder]
     */
    fun createEncoder(): Sha256Encoder
}

Затем перейдите к реализациям для конкретных платформ.

Внедрение C-кода для целевых платформ iOS

Для целевых платформ iOS мы настроим плагин Gradle C Klib, который создаст наш C-код как битовый код LLVM. .

:::информация C Klib — это экспериментальная библиотека с ограниченной поддержкой авторов.

:::

Все, что нам нужно сделать, это указать путь к исходной папке с C-кодом; в нашем примере это будет native/sha256:

cklib {
    config.kotlinVersion = libs.versions.kotlin.get()
    create("sha256") {
        language = C
        srcDirs = project.files(file("native/sha256"))
    }
}

Затем нам нужно создать файл .def для инструмента KMM cinterop, который генерирует привязки Kotlin из файла C-заголовка.

:::подсказка Чтобы узнать больше о .def, вы можете прочитать официальный учебник по cinterop

:::

В нашем случае нам просто нужно создать наш sha256.def в папке nativeInterop/cinterop (место поиска по умолчанию для инструмента cinterop) и назвать package, где будут привязки Kotlin:

package = com.ttypic.clibs.sha256

Наконец, нам нужно добавить шаг cinterop для наших целевых платформ и указать заголовочный файл:

   kotlin {
    // ...
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.compilations {
            val main by getting {
                cinterops {
                    create("sha256") {
                        header(file("native/sha256/sha256.h"))
                    }
                }
            }
        }
    }
    // ...
}

После повторной синхронизации нашей сборки Gradle мы увидим привязки Kotlin в пакете com.ttypic.clibs.sha256 и готовы вызывать нативный код. Наша фактическая реализация будет выглядеть так:

actual object Sha256Factory {
    actual fun createEncoder(): Sha256Encoder = NativeSha256Encoder
}

object NativeSha256Encoder : Sha256Encoder {
    @OptIn(ExperimentalUnsignedTypes::class)
    override fun encode(src: ByteArray): ByteArray =
        memScoped {
            val ctx = alloc<SHA256_CTX>()
            sha256_init(ctx.ptr)
            val srcPointer = src.toUByteArray().toCValues().ptr
            sha256_update(ctx.ptr, srcPointer, src.size.toULong())
            UByteArray(32).apply {
                usePinned {
                    sha256_final(ctx.ptr, it.addressOf(0))
                }
            }.toByteArray()
        }
}

Вот и все; теперь мы можем запустить тесты, чтобы проверить, все ли в порядке:

class NativeSha256Test {

    @Test
    fun `should pass library sha256 checks`() {
        checkEncodeToString("Kotlin", "c78f6c97923e81a2f04f09c5e87b69e085c1e47066a1136b5f590bfde696e2eb")
        checkEncodeToString("abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
        checkEncodeToString("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")
        checkEncodeToString("aaaaaaaaaa", "bf2cb58a68f684d95a3b78ef8f661c9a4e5b09e82cc8f9cc88cce90528caeb27")
    }

    private fun checkEncodeToString(input: String, expectedOutput: String) {
        assertEquals(expectedOutput, Sha256Factory.createEncoder().encodeToString(input.asciiToByteArray()))
    }

}

private fun String.asciiToByteArray() = ByteArray(length) {
    get(it).code.toByte()
}

Внедрение C-кода для целевых платформ Android

Для Android мы будем использовать Android NDK. Android NDK внутри использует CMake; вот почему нам нужно предоставить файл сборки CMakeList.txt:

cmake_minimum_required(VERSION 3.4.1)
# setup C standard we used
set(CMAKE_C_STANDARD 99)
# source code file mask
file(GLOB_RECURSE sources "../../native/*.c")
# build sources as dynamic library 
add_library(sha256 SHARED ${sources})
# link library
target_link_libraries(sha256)

Затем мы должны указать этот файл в плагине Android и настроить целевые платформы:

android {
    // ...
    defaultConfig {
        // ...
        ndk {
            // target platforms
            abiFilters += listOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a")
        }
    }
    // ...
    externalNativeBuild {
        cmake {
            path = file("src/androidMain/CMakeLists.txt")
        }
    }
}

Чтобы вызвать наш собственный код, нам нужно использовать JNI для сопоставить собственный код с JVM. Android Studio имеет отличную поддержку JNI. Давайте создадим файл sha256-jni.c, затем подключим JNI и заголовочные файлы нашей библиотеки:

#include "jni.h"
#include "sha256/sha256.h"

Теперь мы готовы написать наш фактический код:

actual object Sha256Factory {
    actual fun createEncoder(): Sha256Encoder = AndroidSha256Encoder
}

object AndroidSha256Encoder : Sha256Encoder {
    init {
        System.loadLibrary("sha256")
    }
    external override fun encode(src: ByteArray): ByteArray
}

Это очень просто. Сначала мы загружаем нашу динамическую библиотеку, а затем используем JNI для вызова собственного кода. Теперь все, что нам нужно, это написать JNI-привязки. Вы можете сделать это самостоятельно, используя полное имя класса, или вызвать контекстное меню Android Studio и выбрать создать функцию JNI

.

Create JNI function from context-menu

Функция JNI в sha256-jni.c

JNIEXPORT jbyteArray JNICALL Java_com_ttypic_clibs_AndroidSha256Encoder_encode(JNIEnv *env, jclass _, jbyteArray src) {
    BYTE hash[SHA256_BLOCK_SIZE];
    SHA256_CTX ctx;

    size_t len = (size_t) ((*env)->GetArrayLength(env, src));
    jboolean copied;
    jbyte* bytes = (*env)->GetByteArrayElements(env, src, &copied);

    sha256_init(&ctx);
    sha256_update(&ctx, (const BYTE *) bytes, len);
    sha256_final(&ctx, hash);

    (*env)->ReleaseByteArrayElements(env, src, bytes, JNI_ABORT);

    jbyteArray result = (*env)->NewByteArray(env, SHA256_BLOCK_SIZE);
    (*env)->SetByteArrayRegion(env, result, 0, SHA256_BLOCK_SIZE, (const jbyte *) hash);
    return result;
}

И, наконец, мы готовы писать тесты:

:::информация Нам нужно написать инструментальный тест на симуляторе; обычный модульный тест не загрузит нашу нативную библиотеку.

:::

class AndroidSha256Test {

    @Test
    fun should_pass_library_sha256_checks() {
        checkEncodeToString("Kotlin", "c78f6c97923e81a2f04f09c5e87b69e085c1e47066a1136b5f590bfde696e2eb")
        checkEncodeToString("abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
        checkEncodeToString("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1")
        checkEncodeToString("aaaaaaaaaa", "bf2cb58a68f684d95a3b78ef8f661c9a4e5b09e82cc8f9cc88cce90528caeb27")
    }

    private fun checkEncodeToString(input: String, expectedOutput: String) {
        assertEquals(
            expectedOutput,
            Sha256Factory.createEncoder().encodeToString(input.asciiToByteArray())
        )
    }
}

private fun String.asciiToByteArray() = ByteArray(length) {
    get(it).code.toByte()
}

Мы успешно внедрили C-код в нашу мультиплатформенную библиотеку Kotlin, которую можно использовать на целевых платформах iOS и Android.

Полезные ссылки


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE