Skip to content

Commit 1153816

Browse files
authored
update: third tutorial revamp (#4736)
1 parent a616905 commit 1153816

File tree

3 files changed

+84
-181
lines changed

3 files changed

+84
-181
lines changed

docs/topics/native/mapping-function-pointers-from-c.md

Lines changed: 81 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,21 @@
1818
>
1919
{style="warning"}
2020

21-
In this tutorial, you'll learn how to:
21+
Let's explore which C function pointers are visible from Kotlin and examine advanced C interop-related use cases of
22+
Kotlin/Native and [multiplatform](gradle-configure-project.md#targeting-multiple-platforms) Gradle builds.
2223

23-
- [Pass Kotlin function as C function pointer](#pass-kotlin-function-as-c-function-pointer)
24-
- [Use C function pointer from Kotlin](#use-the-c-function-pointer-from-kotlin)
24+
In this tutorial, you'll:
25+
26+
* [Learn how to pass Kotlin function as a C function pointer](#pass-kotlin-function-as-a-c-function-pointer)
27+
* [Use C function pointers from Kotlin](#use-the-c-function-pointer-from-kotlin)
2528

2629
## Mapping function pointer types from C
2730

28-
The best way to understand the mapping between Kotlin and C is to try a tiny
29-
example. Declare a function that accepts a function pointer as a parameter and
30-
another function that returns a function pointer.
31+
To understand the mapping between Kotlin and C, let's declare two functions: one that accepts a function pointer as a
32+
parameter and another that returns a function pointer.
3133

32-
Kotlin/Native comes with the `cinterop` tool; the tool generates bindings between the C language and Kotlin.
33-
It uses a `.def` file to specify a C library to import. More details on this are
34-
in [Interop with C Libraries](native-c-interop.md).
35-
36-
The quickest way to try out C API mapping is to have all C declarations in the
37-
`interop.def` file, without creating any `.h` of `.c` files at all. Then place the C declarations
38-
in a `.def` file after the special `---` separator line:
34+
In the [first part of the series](mapping-primitive-data-types-from-c.md) of the series, you've already created a C library with the
35+
necessary files. For this step, update the declarations in the `interop.def` file after the `---` separator:
3936

4037
```c
4138

@@ -54,216 +51,121 @@ void accept_fun(MyFun f) {
5451
MyFun supply_fun() {
5552
return myFun;
5653
}
57-
5854
```
5955
60-
The `interop.def` file is enough to compile and run the application or open it in an IDE.
61-
Now it is time to create project files, open the project in
62-
[IntelliJ IDEA](https://jetbrains.com/idea) and run it.
56+
The `interop.def` file provides everything necessary to compile, run, or open the application in an IDE.
6357
6458
## Inspect generated Kotlin APIs for a C library
6559
66-
While it is possible to use the command line, either directly or
67-
by combining it with a script file (such as `.sh` or `.bat` file), this approach doesn't
68-
scale well for big projects that have hundreds of files and libraries.
69-
It is then better to use the Kotlin/Native compiler with a build system, as it
70-
helps to download and cache the Kotlin/Native compiler binaries and libraries with
71-
transitive dependencies and run the compiler and tests.
72-
Kotlin/Native can use the [Gradle](https://gradle.org) build system through the [kotlin-multiplatform](gradle-configure-project.md#targeting-multiple-platforms) plugin.
73-
74-
We covered the basics of setting up an IDE compatible project with Gradle in the
75-
[Get started with Kotlin/Native](native-get-started.md#using-gradle)
76-
tutorial. Please check it out if you are looking for detailed first steps
77-
and instructions on how to start a new Kotlin/Native project and open it in IntelliJ IDEA.
78-
In this tutorial, we'll look at the advanced C interop related usages of Kotlin/Native
79-
and [multiplatform](gradle-configure-project.md#targeting-multiple-platforms)
80-
builds with Gradle.
81-
82-
First, create a project folder. All the paths in this tutorial will be relative to this folder. Sometimes
83-
the missing directories will have to be created before any new files can be added.
60+
Let's see how C function pointers are mapped into Kotlin/Native and update your project:
8461
85-
Use the following `build.gradle(.kts)` Gradle build file:
62+
1. In `src/nativeMain/kotlin`, update your `hello.kt` file from the [previous tutorial](mapping-struct-union-types-from-c.md)
63+
with the following content:
8664
87-
<tabs group="build-script">
88-
<tab title="Kotlin" group-key="kotlin">
89-
90-
```kotlin
91-
plugins {
92-
kotlin("multiplatform") version "%kotlinVersion%"
93-
}
65+
```kotlin
66+
import interop.*
67+
import kotlinx.cinterop.ExperimentalForeignApi
68+
69+
@OptIn(ExperimentalForeignApi::class)
70+
fun main() {
71+
println("Hello Kotlin/Native!")
72+
73+
accept_fun(/* fix me*/)
74+
val useMe = supply_fun()
75+
}
76+
```
9477

95-
repositories {
96-
mavenCentral()
97-
}
78+
2. Use the IntelliJ IDEA's [Go to declaration](https://www.jetbrains.com/help/rider/Navigation_and_Search__Go_to_Declaration.html)
79+
command (<shortcut>Cmd + B</shortcut>/<shortcut>Ctrl + B</shortcut>) to navigate to the following generated API
80+
for C functions:
9881

99-
kotlin {
100-
linuxX64("native") { // on Linux
101-
// macosX64("native") { // on x86_64 macOS
102-
// macosArm64("native") { // on Apple Silicon macOS
103-
// mingwX64("native") { // on Windows
104-
val main by compilations.getting
105-
val interop by main.cinterops.creating
106-
107-
binaries {
108-
executable()
109-
}
110-
}
111-
}
82+
```kotlin
83+
fun myFun(i: kotlin.Int): kotlin.Int
84+
fun accept_fun(f: kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>? /* from: interop.MyFun? */)
85+
fun supply_fun(): kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>? /* from: interop.MyFun? */
86+
```
11287

113-
tasks.wrapper {
114-
gradleVersion = "%gradleVersion%"
115-
distributionType = Wrapper.DistributionType.BIN
116-
}
117-
```
88+
As you can see, C function pointers are represented in Kotlin using `CPointer<CFunction<...>>`. The `accept_fun()` function
89+
takes an optional function pointer as a parameter, while `supply_fun()` returns a function pointer.
11890

119-
</tab>
120-
<tab title="Groovy" group-key="groovy">
91+
`CFunction<(Int) -> Int>` represents the function signature, and `CPointer<CFunction<...>>?` represents a nullable
92+
function pointer. There is an `invoke` operator extension function available for all `CPointer<CFunction<...>>` types,
93+
allowing you to call function pointers as if they were regular Kotlin functions.
12194

122-
```groovy
123-
plugins {
124-
id 'org.jetbrains.kotlin.multiplatform' version '%kotlinVersion%'
125-
}
95+
## Pass Kotlin function as a C function pointer
12696

127-
repositories {
128-
mavenCentral()
129-
}
130-
131-
kotlin {
132-
linuxX64('native') { // on Linux
133-
// macosX64("native") { // on x86_64 macOS
134-
// macosArm64("native") { // on Apple Silicon macOS
135-
// mingwX64('native') { // on Windows
136-
compilations.main.cinterops {
137-
interop
138-
}
139-
140-
binaries {
141-
executable()
142-
}
143-
}
144-
}
145-
146-
wrapper {
147-
gradleVersion = '%gradleVersion%'
148-
distributionType = 'BIN'
149-
}
150-
```
151-
152-
</tab>
153-
</tabs>
154-
155-
The project file configures the C interop as an additional step of the build.
156-
Let's move the `interop.def` file to the `src/nativeInterop/cinterop` directory.
157-
Gradle recommends using conventions instead of configurations,
158-
for example, the source files are expected to be in the `src/nativeMain/kotlin` folder.
159-
By default, all the symbols from C are imported to the `interop` package,
160-
you may want to import the whole package in our `.kt` files.
161-
Check out the [Multiplatform Gradle DSL reference](multiplatform-dsl-reference.md) to learn about all the different ways you could configure it.
162-
163-
Let's create a `src/nativeMain/kotlin/hello.kt` stub file with the following content
164-
to see how C function pointer declarations are visible from Kotlin:
97+
It's time to try using C functions from Kotlin code. Call the `accept_fun()` function and pass the C function pointer
98+
to a Kotlin lambda:
16599

166100
```kotlin
167101
import interop.*
102+
import kotlinx.cinterop.staticCFunction
103+
import kotlinx.cinterop.ExperimentalForeignApi
168104

169-
fun main() {
170-
println("Hello Kotlin/Native!")
171-
172-
accept_fun(https://kotlinlang.org/*fix me */)
173-
val useMe = supply_fun()
105+
@OptIn(ExperimentalForeignApi::class)
106+
fun myFun() {
107+
accept_fun(staticCFunction<Int, Int> { it + 1 })
174108
}
175109
```
176110

177-
Now you are ready to
178-
[open the project in IntelliJ IDEA](native-get-started.md)
179-
and to see how to fix the example project. While doing that,
180-
see how C functions are mapped into Kotlin/Native declarations.
111+
This call uses the `staticCFunction {}` helper function from Kotlin/Native to wrap a Kotlin lambda function into a C
112+
function pointer. It allows only unbound and non-capturing lambda functions. For example, it cannot capture a local
113+
variable from the function, only globally visible declarations.
114+
115+
Ensure that the function doesn't throw any exceptions. Throwing exceptions from a `staticCFunction {}`
116+
causes non-deterministic side effects.
181117

182-
## C function pointers in Kotlin
118+
## Use the C function pointer from Kotlin
183119

184-
With the help of IntelliJ IDEA's **Go To** | **Declaration or Usages** or
185-
compiler errors, see the following declarations for the C functions:
120+
The next step is to invoke a C function pointer returned from the `supply_fun()` call:
186121

187122
```kotlin
188-
fun accept_fun(f: MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */)
189-
fun supply_fun(): MyFun? /* = CPointer<CFunction<(Int) -> Int>>? */
190-
191-
fun myFun(i: kotlin.Int): kotlin.Int
123+
import interop.*
124+
import kotlinx.cinterop.ExperimentalForeignApi
125+
import kotlinx.cinterop.invoke
192126

193-
typealias MyFun = kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>
127+
@OptIn(ExperimentalForeignApi::class)
128+
fun myFun2() {
129+
val functionFromC = supply_fun() ?: error("No function is returned")
194130

195-
typealias MyFunVar = kotlinx.cinterop.CPointerVarOf<lib.MyFun>
131+
functionFromC(42)
132+
}
196133
```
197134

198-
You see that the function's `typedef` from C has been turned into Kotlin `typealias`. It uses `CPointer<..>` type
199-
to represent the pointer parameters, and `CFunction<(Int)->Int>` to represent the function signature.
200-
There is an `invoke` operator extension function available for all `CPointer<CFunction<..>` types, so that
201-
it is possible to call it as you would call any other function in Kotlin.
135+
Kotlin turns the function pointer return type into a nullable `CPointer<CFunction<>` object. You need to first explicitly
136+
check for `null`, which is why the [Elvis operator](null-safety.md) is used in the code above.
137+
The `cinterop` tool allows you to call a C function pointer as a regular Kotlin function call: `functionFromC(42)`.
202138

203-
## Pass Kotlin function as C function pointer
139+
## Update Kotlin code
204140

205-
It is the time to try using C functions from the Kotlin program. Call the `accept_fun`
206-
function and pass the C function pointer to a Kotlin lambda:
141+
Now that you've seen all the definitions, try to use them in your project.
142+
The code in the `hello.kt` file may look like this:
207143

208144
```kotlin
209-
fun myFun() {
210-
accept_fun(staticCFunction<Int, Int> { it + 1 })
211-
}
212-
213-
```
214-
215-
This call uses the `staticCFunction{..}` helper function from Kotlin/Native to wrap a Kotlin lambda function into a C function pointer.
216-
It only allows having unbound and non-capturing lambda functions. For example, it is not able
217-
to use a local variable from the function. You may only use globally visible declarations.
218-
219-
It is vital to make sure that the function does not throw any exceptions.
220-
Throwing exceptions from a `staticCFunction{..}` will end up in non-deterministic side-effects.
145+
import interop.*
146+
import kotlinx.cinterop.ExperimentalForeignApi
147+
import kotlinx.cinterop.invoke
148+
import kotlinx.cinterop.staticCFunction
221149

222-
## Use the C function pointer from Kotlin
150+
@OptIn(ExperimentalForeignApi::class)
151+
fun main() {
152+
println("Hello Kotlin/Native!")
223153

224-
The next step is to call a C function pointer from a C pointer that you have from the `supply_fun()` call:
154+
val cFunctionPointer = staticCFunction<Int, Int> { it + 1 }
155+
accept_fun(cFunctionPointer)
225156

226-
```kotlin
227-
fun myFun2() {
228-
val functionFromC = supply_fun() ?: error("No function is returned")
229-
230-
functionFromC(42)
157+
val funFromC = supply_fun() ?: error("No function is returned")
158+
funFromC(42)
231159
}
232-
233160
```
234161

235-
Kotlin turns the function pointer return type into a nullable `CPointer<CFunction<..>` object. There is the need
236-
to explicitly check for `null` first. The [elvis operator](null-safety.md) for that in the code above.
237-
The `cinterop` tool helps us to turn a C function pointer into an easy to call object in Kotlin. This is
238-
what we did on the last line.
239-
240-
## Fix the code
241-
242-
You've seen all definitions and it is time to fix and run the code.
243-
Run the `runDebugExecutableNative` Gradle task [in the IDE](native-get-started.md)
162+
To verify that everything works as expected, run the `runDebugExecutableNative` Gradle task [in IDE](native-get-started.md#build-and-run-the-application)
244163
or use the following command to run the code:
245164

246165
```bash
247166
./gradlew runDebugExecutableNative
248167
```
249168

250-
The code in the `hello.kt` file may look like this:
251-
252-
```kotlin
253-
import interop.*
254-
import kotlinx.cinterop.*
255-
256-
fun main() {
257-
println("Hello Kotlin/Native!")
258-
259-
val cFunctionPointer = staticCFunction<Int, Int> { it + 1 }
260-
accept_fun(cFunctionPointer)
261-
262-
val funFromC = supply_fun() ?: error("No function is returned")
263-
funFromC(42)
264-
}
265-
```
266-
267169
## Next step
268170

269171
In the next part of the series, you'll learn how strings are mapped between Kotlin and C:

docs/topics/native/mapping-primitive-data-types-from-c.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ To create a C library:
105105
void doubles(float a, double b) { }
106106
```
107107
108-
The `interop.def` file is enough to compile and run the application or open it in an IDE.
108+
The `interop.def` file provides everything necessary to compile, run, or open the application in an IDE.
109109
110110
## Create a Kotlin/Native project
111111

docs/topics/native/mapping-struct-union-types-from-c.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ void union_by_value(MyUnion u) {}
5757
void union_by_pointer(MyUnion* u) {}
5858
```
5959
60-
The `interop.def` file is enough to compile and run the application or open it in an IDE.
60+
The `interop.def` file provides everything necessary to compile, run, or open the application in an IDE.
6161
6262
## Inspect generated Kotlin APIs for a C library
6363
@@ -313,6 +313,7 @@ import kotlinx.cinterop.cValue
313313
import kotlinx.cinterop.memScoped
314314
import kotlinx.cinterop.ptr
315315
import kotlinx.cinterop.readValue
316+
import kotlinx.cinterop.ExperimentalForeignApi
316317

317318
@OptIn(ExperimentalForeignApi::class)
318319
fun main() {

0 commit comments

Comments
 (0)