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 빌드 시스템을 사용할 수 있어요.

출처: Mapping primitive data types from C – tutorial

본문

C 언어의 타입

C 프로그래밍 언어에는 다음 데이터 타입이 있어요:

  • 기본 타입: char, int, float, double 그리고 수식어 signed, unsigned, short, long
  • 구조체(structure), 공용체(union), 배열(array)
  • 포인터
  • 함수 포인터

더 구체적인 타입도 있어요:

  • 불리언 타입(C99부터)
  • size_tptrdiff_t(ssize_t도 있음)
  • int32_tuint64_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 라이브러리를 만들려면:

  1. 앞으로 만들 프로젝트를 위한 빈 폴더를 만들어요.
  2. 안에 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 스레드를 참조해요.

  1. 다음 내용으로 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 시작하기 튜토리얼을 참조해요.

프로젝트 파일을 만들려면:

  1. 프로젝트 폴더에 다음 내용으로 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 참조를 확인해요.

  1. interop.def, lib.h, lib.def 파일을 src/nativeInterop/cinterop 디렉터리로 옮겨요.
  2. src/nativeMain/kotlin 디렉터리를 만들어요. 모든 소스 파일은 Gradle의 소스 세트 레이아웃 권장 사항에 따라 이곳에 두면 돼요. 기본적으로 C의 모든 심볼은 interop 패키지로 가져와져요.
  3. 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) 문서에서 더 자세히 알아보세요.