Kotlin/Native as a dynamic library – tutorial
You can create dynamic libraries to use Kotlin code from existing programs. This enables code sharing across many platforms or languages, including JVM, Python, Android, and others.
You can use the Kotlin/Native code from existing native applications or libraries. For this, you need to compile the Kotlin code into a dynamic library in the .so
, .dylib
, or .dll
format.
In this tutorial, you will:
You can use the command line to generate a Kotlin library, either directly or with a script file (such as .sh
or .bat
file). However, this approach doesn't scale well for big projects that have hundreds of files and libraries. Using a build system simplifies the process by downloading and caching the Kotlin/Native compiler binaries and libraries with transitive dependencies, and by running the compiler and tests. Kotlin/Native can use the Gradle build system through the Kotlin Multiplatform plugin.
Let's examine the advanced C interop-related usages of Kotlin/Native and Kotlin Multiplatform builds with Gradle.
Create a Kotlin library
The Kotlin/Native compiler can produce a dynamic library from the Kotlin code. A dynamic library often comes with a .h
header file, which you use to call the compiled code from C.
Let's create a Kotlin library and use it from a C program.
Navigate to the
src/nativeMain/kotlin
directory and create thelib.kt
file with the following library contents:package example object Object { val field = "A" } class Clazz { fun memberFunction(p: Int): ULong = 42UL } fun forIntegers(b: Byte, s: Short, i: UInt, l: Long) { } fun forFloats(f: Float, d: Double) { } fun strings(str: String) : String? { return "That is '$str' from C" } val globalString = "A global String"Update your
build.gradle(.kts)
Gradle build file with the following:plugins { kotlin("multiplatform") version "2.1.0" } repositories { mavenCentral() } kotlin { macosArm64("native") { // macOS on Apple Silicon // macosX64("native") { // macOS on x86_64 platforms // linuxArm64("native") { // Linux on ARM64 platforms // linuxX64("native") { // Linux on x86_64 platforms // mingwX64("native") { // Windows binaries { sharedLib { baseName = "native" // macOS and Linux // baseName = "libnative" // Windows } } } } tasks.wrapper { gradleVersion = "8.10" distributionType = Wrapper.DistributionType.ALL }plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.1.0' } repositories { mavenCentral() } kotlin { macosArm64("native") { // Apple Silicon macOS // macosX64("native") { // macOS on x86_64 platforms // linuxArm64("native") { // Linux on ARM64 platforms // linuxX64("native") { // Linux on x86_64 platforms // mingwX64("native") { // Windows binaries { sharedLib { baseName = "native" // macOS and Linux // baseName = "libnative" // Windows } } } } wrapper { gradleVersion = "8.10" distributionType = "ALL" }The
binaries {}
block configures the project to generate a dynamic or shared library.libnative
is used as the library name, the prefix for the generated header file name. It also prefixes all declarations in the header file.
Run the
linkDebugSharedNative
Gradle task in the IDE or use the following console command in your terminal to build the library:./gradlew linkDebugSharedNative
The build generates the library into the build/bin/native/debugShared
directory with the following files:
macOS
libnative_api.h
andlibnative.dylib
Linux:
libnative_api.h
andlibnative.so
Windows:
libnative_api.h
,libnative.def
, andlibnative.dll
The Kotlin/Native compiler uses the same rules to generate the .h
file for all platforms. Let's check out the C API of the Kotlin library.
Generated header file
Let's examine how Kotlin/Native declarations are mapped to C functions.
In the build/bin/native/debugShared
directory, open the libnative_api.h
header file. The very first part contains the standard C/C++ header and footer:
Following this, the libnative_api.h
includes a block with the common type definitions:
Kotlin uses the libnative_
prefix for all declarations in the created libnative_api.h
file. Here's the complete list of type mappings:
Kotlin definition | C type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The definition section of the libnative_api.h
file shows how Kotlin primitive types are mapped to C primitive types. The Kotlin/Native compiler generates these entries automatically for every library. The reverse mapping is described in the Mapping primitive data types from C tutorial.
After the automatically generated type definitions, you'll find the separate type definitions used in your library:
In C, the typedef struct { ... } TYPE_NAME
syntax declares the structure.
As you can see from these definitions, Kotlin types are mapped using the same pattern: Object
is mapped to libnative_kref_example_Object
, and Clazz
is mapped to libnative_kref_example_Clazz
. All structs contain nothing but the pinned
field with a pointer. The field type libnative_KNativePtr
is defined as void*
earlier in the file.
Since C doesn't support namespaces, the Kotlin/Native compiler generates long names to avoid any possible clashes with other symbols in the existing native project.
Service runtime functions
The libnative_ExportedSymbols
structure defines all the functions provided by Kotlin/Native and your library. It uses nested anonymous structures heavily to mimic packages. The libnative_
prefix comes from the library name.
libnative_ExportedSymbols
includes several helper functions in the header file:
These functions deal with Kotlin/Native objects. DisposeStablePointer
is called to release a reference to a Kotlin object, and DisposeString
is called to release a Kotlin string, which has the char*
type in C.
The next part of the libnative_api.h
file consists of structure declarations of runtime functions:
You can use the IsInstance
function to check if a Kotlin object (referenced with its .pinned
pointer) is an instance of a type. The actual set of operations generated depends on actual usages.
Your library functions
Let's take a look at the separate structure declarations used in your library. The libnative_kref_example
field mimics the package structure of your Kotlin code with a libnative_kref.
prefix:
The code uses anonymous structure declarations. Here, struct { ... } foo
declares a field in the outer struct of the anonymous structure type, which has no name.
Since C doesn't support objects either, function pointers are used to mimic object semantics. A function pointer is declared as RETURN_TYPE (* FIELD_NAME)(PARAMETERS)
.
The libnative_kref_example_Clazz
field represents the Clazz
from Kotlin. The libnative_KULong
is accessible with the memberFunction
field. The only difference is that the memberFunction
accepts a thiz
reference as the first parameter. Since C doesn't support objects, the thiz
pointer is passed explicitly.
There is a constructor in the Clazz
field (aka libnative_kref_example_Clazz_Clazz
), which acts as the constructor function to create an instance of the Clazz
.
The Kotlin object Object
is accessible as libnative_kref_example_Object
. The _instance
function retrieves the only instance of the object.
Properties are translated into functions. The get_
and set_
prefixes name the getter and the setter functions, respectively. For example, the read-only property globalString
from Kotlin is turned into a get_globalString
function in C.
Global functions forFloats
, forIntegers
, and strings
are turned into functions pointers in the libnative_kref_example
anonymous struct.
Entry point
Now you know how the API is created, the initialization of the libnative_ExportedSymbols
structure is the starting point. Let's then take a look at the final part of the libnative_api.h
:
The libnative_symbols
function allows you to open the gateway from the native code to the Kotlin/Native library. This is the entry point for accessing the library. The library name is used as a prefix for the function name.
Use generated headers from C
Using the generated headers from C is straightforward. In the library directory, create the main.c
file with the following code:
Compile and run the project
On macOS
To compile the C code and link it with the dynamic library, navigate to the library directory and run the following command:
The compiler generates an executable called a.out
. Run it to execute the Kotlin code from the C library.
On Linux
To compile the C code and link it with the dynamic library, navigate to the library directory and run the following command:
The compiler generates an executable called a.out
. Run it to execute the Kotlin code from the C library. On Linux, you need to include .
into the LD_LIBRARY_PATH
to let the application know to load the libnative.so
library from the current folder.
On Windows
First, you'll need to install a Microsoft Visual C++ compiler that supports the x64_64 target.
The easiest way to do this is to install Microsoft Visual Studio on a Windows machine. During installation, select the necessary components to work with C++, for example, Desktop development with C++.
On Windows, you can include dynamic libraries either by generating a static library wrapper or manually with the LoadLibrary or similar Win32API functions.
Let's use the first option and generate the static wrapper library for the libnative.dll
:
Call
lib.exe
from the toolchain to generate the static library wrapperlibnative.lib
that automates the DLL usage from the code:lib /def:libnative.def /out:libnative.libCompile your
main.c
into an executable. Include the generatedlibnative.lib
into the build command and start:cl.exe main.c libnative.libThe command produces the
main.exe
file, which you can run.