Как расширить общий модуль 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 a> для вызова собственного кода. Теперь все, что нам нужно, это написать JNI-привязки. Вы можете сделать это самостоятельно, используя полное имя класса, или вызвать контекстное меню Android Studio и выбрать создать функцию JNI
Функция 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.
Полезные ссылки
- Репозиторий Github с окончательным кодом
- Начало работы с KMM
- Официальное руководство по библиотеке KMM
- Проект Zipline
- Встроенные зависимости в KMM
Оригинал