visit
Kotlin Multiplatform Mobile (KMM) is an SDK designed to simplify the development of cross-platform mobile applications. You can share common code between iOS and Android apps and write platform-specific code only where it's necessary
You can use C and C++ code both with Android and iOS (Xcode supports C, C++, and out-of-the-box, and Android has Android NDK). KMM utilizes this functionality and allows you to use C and C++ code in your shared module.
Let’s take the SHA-256 code written on C as an example, and copy it into our native/sha256
folder. The sample code for the SHA-256 implementation was taken from this .
/**
* 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
}
All we need to do is provide a path to the source folder with C-code; in our example, it will be native/sha256
:
cklib {
config.kotlinVersion = libs.versions.kotlin.get()
create("sha256") {
language = C
srcDirs = project.files(file("native/sha256"))
}
}
Then we need to create a .def
file for the KMM cinterop
tool which generates Kotlin bindings from a C-header file.
To learn more about .def
, you could read
In our case, we just need to create our sha256.def
in the nativeInterop/cinterop
folder (default searching place for cinterop
tool) and name package where Kotlin bindings will be:
package = com.ttypic.clibs.sha256
kotlin {
// ...
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.compilations {
val main by getting {
cinterops {
create("sha256") {
header(file("native/sha256/sha256.h"))
}
}
}
}
}
// ...
}
After resyncing our Gradle build, we will see Kotlin bindings in the com.ttypic.clibs.sha256
package, and we are ready to invoke native code. Our actual
implementation will look like this:
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()
}
For Android, we will be using . Android NDK uses under the hood; that’s why we need to provide the CMakeList.txt
build file:
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 {
// ...
defaultConfig {
// ...
ndk {
// target platforms
abiFilters += listOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a")
}
}
// ...
externalNativeBuild {
cmake {
path = file("src/androidMain/CMakeLists.txt")
}
}
}
To invoke our native code, we need to use to map native code to JVM. Android Studio has great JNI support. Let’s create sha256-jni.c
file, then include JNI and our library header files:
#include "jni.h"
#include "sha256/sha256.h"
Now, we are ready to write our actual
code:
actual object Sha256Factory {
actual fun createEncoder(): Sha256Encoder = AndroidSha256Encoder
}
object AndroidSha256Encoder : Sha256Encoder {
init {
System.loadLibrary("sha256")
}
external override fun encode(src: ByteArray): ByteArray
}
It is very straightforward. First, we load our dynamic library, and next, we use to invoke native code. Now, all we need is to write JNI-bindings. You can do it yourself using the fully qualified class name or invoke the Android Studio context menu and choose create JNI funtion
JNI function in 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()
}