C에서 기본 데이터 타입 매핑하기
C에서 기본 데이터 타입 매핑하기
참고: C 라이브러리 임포트는 Beta 상태예요. cinterop 도구가 C 라이브러리에서 생성하는 모든 Kotlin 선언에는
@ExperimentalForeignApi어노테이션이 붙어요. Kotlin/Native와 함께 제공되는 네이티브 플랫폼 라이브러리(Foundation, UIKit, POSIX 등)는 일부 API에 대해서만 옵트인이 필요해요.
이 튜토리얼에서는 어떤 C 데이터 타입이 Kotlin/Native에서 보이는지(그 반대도) 살펴보고, Kotlin/Native와 멀티플랫폼 Gradle 빌드의 고급 C 상호 운용(interop) 사용 사례를 알아봐요.
이 튜토리얼에서 여러분은 다음을 배워요:
- C 언어의 데이터 타입 알아보기
- 내보내기(export)에서 그 타입들을 사용하는 C 라이브러리 만들기
- C 라이브러리에서 생성된 Kotlin API 살펴보기
명령줄을 사용해 직접 또는 스크립트 파일(예: .sh나 .bat 파일)로 Kotlin 라이브러리를 생성할 수도 있어요. 다만 이 방식은 수백 개의 파일과 라이브러리가 있는 큰 프로젝트에는 확장성이 좋지 않아요. 빌드 시스템을 사용하면 Kotlin/Native 컴파일러 바이너리와 전이적 의존성을 가진 라이브러리를 다운로드하고 캐시하며, 컴파일러와 테스트를 실행해서 과정을 단순화해요. Kotlin/Native는 Kotlin Multiplatform 플러그인을 통해 Gradle 빌드 시스템을 사용할 수 있어요.
본문
C 언어의 타입
C 프로그래밍 언어에는 다음 데이터 타입이 있어요:
- 기본 타입:
char,int,float,double그리고 수식어signed,unsigned,short,long - 구조체(structure), 공용체(union), 배열(array)
- 포인터
- 함수 포인터
더 구체적인 타입도 있어요:
- 불리언 타입(C99부터)
size_t와ptrdiff_t(ssize_t도 있음)int32_t나uint64_t같은 고정 폭 정수 타입(C99부터)
또한 C 언어에는 const, volatile, restrict, atomic 같은 타입 한정자(type qualifier)도 있어요. 이제 어떤 C 데이터 타입이 Kotlin에서 보이는지 살펴봐요.
C 라이브러리 만들기
이 튜토리얼에서는 lib.c 소스 파일을 만들지 않아요. 그 파일은 C 라이브러리를 컴파일하고 실행하려는 경우에만 필요해요. 이 설정에서는 cinterop 도구를 실행하는 데 필요한 .h 헤더 파일만 있으면 돼요.
cinterop 도구는 각 .h 파일 집합에 대해 Kotlin/Native 라이브러리(.klib 파일)를 생성해요. 생성된 라이브러리는 Kotlin에서 C 라이브러리로의 호출을 연결(bridge)하는 데 도움을 줘요.
C 라이브러리를 만들려면:
- 앞으로 만들 프로젝트를 위한 빈 폴더를 만들어요.
- 안에 C 함수가 Kotlin에 어떻게 매핑되는지 확인하기 위해
lib.h파일을 다음 내용으로 만들어요:
#ifndef LIB2_H_INCLUDED
#define LIB2_H_INCLUDED
void ints(char c, short d, int e, long f);
void uints(unsigned char c, unsigned short d, unsigned int e, unsigned long f);
void doubles(float a, double b);
#endif
이 파일에는 extern "C" 블록이 없어요. 이 예제에는 필요하지 않지만, C++와 오버로드된 함수를 사용한다면 필요할 수 있어요. 자세한 내용은 이 Stackoverflow 스레드를 참조해요.
- 다음 내용으로
lib.def정의 파일을 만들어요:
headers = lib.h
cinterop 도구가 생성한 코드에 매크로나 다른 C 정의를 포함하면 도움이 될 수 있어요. 이렇게 하면 메서드 본문도 컴파일되어 바이너리에 완전히 포함돼요. 이 기능을 사용하면 C 컴파일러 없이도 실행 가능한 예제를 만들 수 있어요. 그렇게 하려면 lib.h 파일의 C 함수 구현을 --- 구분자 뒤의 새 interop.def 파일에 추가해요:
---
void ints(char c, short d, int e, long f) { }
void uints(unsigned char c, unsigned short d, unsigned int e, unsigned long f) { }
void doubles(float a, double b) { }
interop.def 파일은 애플리케이션을 컴파일하고, 실행하고, IDE에서 여는 데 필요한 모든 것을 제공해요.
Kotlin/Native 프로젝트 만들기
첫 단계와 새 Kotlin/Native 프로젝트를 만들고 여는 방법에 대한 자세한 내용은 Kotlin/Native 시작하기 튜토리얼을 참조해요.
프로젝트 파일을 만들려면:
- 프로젝트 폴더에 다음 내용으로
build.gradle(.kts)Gradle 빌드 파일을 만들어요:
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform") version "2.4.20"
}
repositories {
mavenCentral()
}
kotlin {
macosArm64() // macOS on Apple Silicon
// linuxArm64() // Linux on ARM64 platforms
// linuxX64() // Linux on x86_64 platforms
// mingwX64() // Windows on x86_64 platforms
targets.withType<KotlinNativeTarget>().configureEach {
val main by compilations.getting
val interop by main.cinterops.creating
binaries {
executable()
}
}
}
tasks.wrapper {
gradleVersion = "9.7.0"
distributionType = Wrapper.DistributionType.BIN
}
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '2.4.20'
}
repositories {
mavenCentral()
}
kotlin {
macosArm64() // Apple Silicon macOS
// linuxArm64() // Linux on ARM64 platforms
// linuxX64() // Linux on x86_64 platforms
// mingwX64() // Windows
targets.withType(KotlinNativeTarget).configureEach {
compilations.main.cinterops {
interop
}
binaries {
executable()
}
}
}
wrapper {
gradleVersion = '9.7.0'
distributionType = 'BIN'
}
프로젝트 파일은 C interop을 추가 빌드 단계로 구성해요. 구성할 수 있는 다양한 방법을 알아보려면 Multiplatform Gradle DSL 참조를 확인해요.
interop.def,lib.h,lib.def파일을src/nativeInterop/cinterop디렉터리로 옮겨요.src/nativeMain/kotlin디렉터리를 만들어요. 모든 소스 파일은 Gradle의 소스 세트 레이아웃 권장 사항에 따라 이곳에 두면 돼요. 기본적으로 C의 모든 심볼은interop패키지로 가져와져요.src/nativeMain/kotlin에 다음 내용으로hello.kt스텁 파일을 만들어요:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
ints(/* fix me*/)
uints(/* fix me*/)
doubles(/* fix me*/)
}
Kotlin 쪽에서 C 기본 타입 선언이 어떻게 보이는지 배우면서 나중에 이 코드를 완성하게 될 거예요.
C 라이브러리에 대해 생성된 Kotlin API 살펴보기
C 기본 타입이 Kotlin/Native에 어떻게 매핑되는지 확인하고 예제 프로젝트를 그에 맞게 업데이트해 봐요. IntelliJ IDEA의 선언으로 이동(Go to declaration, Cmd + B / Ctrl + B) 명령을 사용해 C 함수에 대해 다음과 같이 생성된 API로 이동해요:
fun ints(c: kotlin.Byte, d: kotlin.Short, e: kotlin.Int, f: kotlin.Long)
fun uints(c: kotlin.UByte, d: kotlin.UShort, e: kotlin.UInt, f: kotlin.ULong)
fun doubles(a: kotlin.Float, b: kotlin.Double)
C 타입은 직접 매핑되지만, char 타입만 예외예요. 보통 8비트 부호 있는 값이기 때문에 kotlin.Byte로 매핑돼요:
| C | Kotlin |
|---|---|
char |
kotlin.Byte |
unsigned char |
kotlin.UByte |
short |
kotlin.Short |
unsigned short |
kotlin.UShort |
int |
kotlin.Int |
unsigned int |
kotlin.UInt |
long long |
kotlin.Long |
unsigned long long |
kotlin.ULong |
float |
kotlin.Float |
double |
kotlin.Double |
Kotlin 코드 업데이트하기
이제 C 정의를 확인했으니 Kotlin 코드를 업데이트할 수 있어요. hello.kt 파일의 최종 코드는 다음과 같을 수 있어요:
import interop.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun main() {
println("Hello Kotlin/Native!")
ints(1, 2, 3, 4)
uints(5u, 6u, 7u, 8u)
doubles(9.0f, 10.0)
}
모든 것이 예상대로 동작하는지 확인하려면 IDE에서 runDebugExecutable<YourTargetName> Gradle 작업을 실행하거나 터미널에서 콘솔 명령을 사용해요. 이 예제에서는:
./gradlew runDebugExecutableMacosArm64
다음 단계
이 시리즈의 다음 파트에서는 struct와 union 타입이 Kotlin과 C 사이에서 어떻게 매핑되는지 배워요:
- 다음 단계 (Next step)
더 알아보기
더 고급 시나리오를 다루는 C와의 상호 운용(Interoperability with C) 문서에서 더 자세히 알아보세요.