C에서 함수 포인터 매핑하기

C에서 함수 포인터 매핑하기

참고: C 라이브러리 임포트는 Beta 상태예요. cinterop 도구가 C 라이브러리에서 생성하는 모든 Kotlin 선언에는 @ExperimentalForeignApi 어노테이션이 붙어요. Kotlin/Native와 함께 제공되는 네이티브 플랫폼 라이브러리(Foundation, UIKit, POSIX 등)는 일부 API에 대해서만 옵트인이 필요해요.

이 튜토리얼에서는 어떤 C 함수 포인터가 Kotlin에서 보이는지 살펴보고, Kotlin/Native와 멀티플랫폼 Gradle 빌드의 고급 C 상호 운용(interop) 사용 사례를 알아봐요.

이 튜토리얼에서 여러분은 다음을 배워요:

  • Kotlin 함수를 C 함수 포인터로 전달하는 방법
  • Kotlin에서 C 함수 포인터 사용하기

출처: Mapping function pointers from C – tutorial

본문

C에서 함수 포인터 타입 매핑하기

Kotlin과 C 사이의 매핑을 이해하기 위해, 함수 포인터를 매개변수로 받는 함수 하나와 함수 포인터를 반환하는 함수 하나를 선언해 봐요. 시리즈의 첫 번째 파트에서 이미 필요한 파일을 가진 C 라이브러리를 만들었어요. 이번 단계에서는 --- 구분자 뒤의 interop.def 파일에 있는 선언을 업데이트해요:


---

int myFun(int i) {
  return i+1;
}

typedef int  (*MyFun)(int);

void accept_fun(MyFun f) {
  f(42);
}

MyFun supply_fun() {
  return myFun;
}

interop.def 파일은 애플리케이션을 컴파일하고, 실행하고, IDE에서 여는 데 필요한 모든 것을 제공해요.

C 라이브러리에 대해 생성된 Kotlin API 살펴보기

C 함수 포인터가 Kotlin/Native에 어떻게 매핑되는지 확인하고 프로젝트를 업데이트해 봐요:

  1. src/nativeMain/kotlin에서 이전 튜토리얼의 hello.kt 파일을 다음 내용으로 업데이트해요:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi

@OptIn(ExperimentalForeignApi::class)
fun main() {
    println("Hello Kotlin/Native!")

    accept_fun(/* fix me*/)
    val useMe = supply_fun()
}
  1. IntelliJ IDEA의 선언으로 이동(Go to declaration, Cmd + B / Ctrl + B) 명령을 사용해 C 함수에 대해 다음과 같이 생성된 API로 이동해요:
fun myFun(i: kotlin.Int): kotlin.Int
fun accept_fun(f: kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>? /* from: interop.MyFun? */)
fun supply_fun(): kotlinx.cinterop.CPointer<kotlinx.cinterop.CFunction<(kotlin.Int) -> kotlin.Int>>? /* from: interop.MyFun? */

보시다시피 C 함수 포인터는 Kotlin에서 CPointer<CFunction<...>>로 표현돼요. accept_fun() 함수는 선택적인 함수 포인터를 매개변수로 받고, supply_fun()은 함수 포인터를 반환해요. CFunction<(Int) -> Int>는 함수 시그니처를 나타내고, CPointer<CFunction<...>>?는 널 가능(nullable) 함수 포인터를 나타내요. 모든 CPointer<CFunction<...>> 타입에 .invoke() 연산자 확장 함수가 제공되어, 일반적인 Kotlin 함수처럼 함수 포인터를 호출할 수 있어요.

Kotlin 함수를 C 함수 포인터로 전달하기

이제 Kotlin 코드에서 C 함수를 사용해 보는 시간이에요. accept_fun() 함수를 호출하고 C 함수 포인터에 Kotlin 람다를 전달해요:

import interop.*
import kotlinx.cinterop.staticCFunction
import kotlinx.cinterop.ExperimentalForeignApi

@OptIn(ExperimentalForeignApi::class)
fun myFun() {
    accept_fun(staticCFunction<Int, Int> { it + 1 })
}

이 호출은 Kotlin/Native의 staticCFunction {} 헬퍼 함수를 사용해 Kotlin 람다 함수를 C 함수 포인터로 감싸요. 이 함수는 바인딩되지 않고 캡처하지 않는(unbound and non-capturing) 람다 함수만 허용해요. 예를 들어 함수의 지역 변수를 캡처할 수 없고, 전역에서 보이는 선언만 캡처할 수 있어요. 함수가 예외를 던지지 않는지 확인해요. staticCFunction {}에서 예외를 던지면 비결정적(non-deterministic) 부작용이 발생해요.

Kotlin에서 C 함수 포인터 사용하기

다음 단계는 supply_fun() 호출에서 반환된 C 함수 포인터를 호출하는 거예요:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.invoke

@OptIn(ExperimentalForeignApi::class)
fun myFun2() {
    val functionFromC = supply_fun() ?: error("No function is returned")

    functionFromC(42)
}

Kotlin은 함수 포인터 반환 타입을 널 가능한 CPointer<CFunction<>> 객체로 바꿔요. 먼저 null인지 명시적으로 확인해야 하며, 그래서 위 코드에서 엘비스 연산자(Elvis operator)를 사용했어요. cinterop 도구를 사용하면 C 함수 포인터를 일반적인 Kotlin 함수 호출처럼 호출할 수 있어요: functionFromC(42).

Kotlin 코드 업데이트하기

이제 모든 정의를 확인했으니 프로젝트에서 사용해 보세요. hello.kt 파일의 코드는 다음과 같을 수 있어요:

import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.invoke
import kotlinx.cinterop.staticCFunction

@OptIn(ExperimentalForeignApi::class)
fun main() {
    println("Hello Kotlin/Native!")

    val cFunctionPointer = staticCFunction<Int, Int> { it + 1 }
    accept_fun(cFunctionPointer)

    val funFromC = supply_fun() ?: error("No function is returned")
    funFromC(42)
}

모든 것이 예상대로 동작하는지 확인하려면 IDE에서 runDebugExecutable<YourTargetName> Gradle 작업을 실행하거나 터미널에서 콘솔 명령을 사용해요. 이 예제에서는:

./gradlew runDebugExecutableMacosArm64

다음 단계

이 시리즈의 다음 파트에서는 문자열이 Kotlin과 C 사이에서 어떻게 매핑되는지 배워요:

  • 이전 단계 (Previous step)
  • 다음 단계 (Next step)

더 알아보기

더 고급 시나리오를 다루는 C와의 상호 운용(Interoperability with C) 문서에서 더 자세히 알아보세요.