Skip to content

C 상호 운용 및 libcurl을 사용하여 앱 생성하기 – 튜토리얼

C 라이브러리 임포트는 베타 단계입니다. C 라이브러리에서 cinterop 도구로 생성된 모든 Kotlin 선언에는 @ExperimentalForeignApi 어노테이션이 있어야 합니다.

Kotlin/Native와 함께 제공되는 네이티브 플랫폼 라이브러리(예: Foundation, UIKit, POSIX)는 일부 API에 대해서만 옵트인(opt-in)이 필요합니다.

이 튜토리얼은 IntelliJ IDEA를 사용하여 커맨드라인 애플리케이션을 생성하는 방법을 보여줍니다. Kotlin/Native와 libcurl 라이브러리를 사용하여 지정된 플랫폼에서 네이티브로 실행할 수 있는 간단한 HTTP 클라이언트를 만드는 방법을 배울 것입니다.

결과물은 macOS 및 Linux에서 실행하여 간단한 HTTP GET 요청을 할 수 있는 실행 가능한 커맨드라인 앱입니다.

커맨드라인을 사용하여 Kotlin 라이브러리를 직접 생성하거나 스크립트 파일(예: .sh 또는 .bat 파일)로 생성할 수 있습니다. 하지만 이 접근 방식은 수백 개의 파일과 라이브러리가 있는 대규모 프로젝트에는 적합하지 않습니다. 빌드 시스템을 사용하면 Kotlin/Native 컴파일러 바이너리와 전이적 의존성을 가진 라이브러리를 다운로드하고 캐싱하며, 컴파일러와 테스트를 실행함으로써 프로세스가 단순해집니다. Kotlin/Native는 Kotlin 멀티플랫폼 플러그인을 통해 Gradle 빌드 시스템을 사용할 수 있습니다.

시작하기 전에

  1. IntelliJ IDEA 최신 버전을 다운로드하고 설치합니다.

  2. IntelliJ IDEA에서 File | New | Project from Version Control을 선택하고 다음 URL을 사용하여 프로젝트 템플릿을 클론합니다.

    none
    https://github.com/Kotlin/kmp-native-wizard
  3. 프로젝트 구조를 살펴봅니다.

    Native application project structure

    템플릿에는 시작하는 데 필요한 파일과 폴더가 포함된 프로젝트가 있습니다. Kotlin/Native로 작성된 애플리케이션은 코드에 플랫폼별 요구 사항이 없으면 다양한 플랫폼을 대상으로 할 수 있다는 점을 이해하는 것이 중요합니다. 코드는 nativeMain 디렉토리에 해당 nativeTest와 함께 배치됩니다. 이 튜토리얼에서는 폴더 구조를 그대로 유지합니다.

  4. 프로젝트 설정이 포함된 빌드 스크립트인 build.gradle.kts 파일을 엽니다. 빌드 파일에서 다음 사항에 특히 주의하십시오.

    kotlin
    kotlin {
        val hostOs = System.getProperty("os.name")
        val isArm64 = System.getProperty("os.arch") == "aarch64"
        val isMingwX64 = hostOs.startsWith("Windows")
        val nativeTarget = when {
            hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
            hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
            hostOs == "Linux" && isArm64 -> linuxArm64("native")
            hostOs == "Linux" && !isArm64 -> linuxX64("native")
            isMingwX64 -> mingwX64("native")
            else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
        }
    
        nativeTarget.apply {
            binaries {
                executable {
                    entryPoint = "main"
                }
            }
        }
    }
    • 대상은 macOS, Linux 및 Windows용 macosArm64, macosX64, linuxArm64, linuxX64, mingwX64를 사용하여 정의됩니다. 지원되는 플랫폼의 전체 목록을 참조하십시오.
    • binaries {} 블록은 바이너리가 생성되는 방식과 애플리케이션의 진입점을 정의합니다. 이 값들은 기본값으로 유지할 수 있습니다.
    • C 상호 운용은 빌드에서 추가 단계로 구성됩니다. 기본적으로 C의 모든 심볼은 interop 패키지로 임포트됩니다. .kt 파일에서 전체 패키지를 임포트할 수도 있습니다. 구성 방법에 대해 자세히 알아보세요.

정의 파일 생성

네이티브 애플리케이션을 작성할 때, HTTP 요청 생성, 디스크 읽기 및 쓰기 등 Kotlin 표준 라이브러리에 포함되지 않은 특정 기능에 접근해야 하는 경우가 많습니다.

Kotlin/Native는 표준 C 라이브러리를 소비하는 데 도움을 주어, 필요한 거의 모든 기능에 대한 전체 생태계를 열어줍니다. Kotlin/Native는 이미 사전 빌드된 플랫폼 라이브러리 세트와 함께 제공되며, 이는 표준 라이브러리에 일부 추가적인 공통 기능을 제공합니다.

상호 운용의 이상적인 시나리오는 C 함수를 Kotlin 함수를 호출하는 것처럼 동일한 시그니처와 컨벤션을 따라 호출하는 것입니다. 이때 cinterop 도구가 유용하게 사용됩니다. 이 도구는 C 라이브러리를 가져와 해당 Kotlin 바인딩을 생성하여 라이브러리가 Kotlin 코드처럼 사용될 수 있도록 합니다.

이러한 바인딩을 생성하려면 각 라이브러리에 정의 파일이 필요하며, 일반적으로 라이브러리와 동일한 이름을 가집니다. 이는 라이브러리가 정확히 어떻게 사용되어야 하는지를 설명하는 속성 파일입니다.

이 앱에서는 HTTP 호출을 위해 libcurl 라이브러리가 필요합니다. 정의 파일을 생성하려면:

  1. src 폴더를 선택하고 File | New | Directory로 새 디렉토리를 생성합니다.

  2. 새 디렉토리 이름을 nativeInterop/cinterop으로 지정합니다. 이는 헤더 파일 위치에 대한 기본 규칙이지만, 다른 위치를 사용하는 경우 build.gradle.kts 파일에서 재정의할 수 있습니다.

  3. 이 새 하위 폴더를 선택하고 File | New | File로 새 libcurl.def 파일을 생성합니다.

  4. 파일에 다음 코드를 업데이트합니다.

    c
    headers = curl/curl.h
    headerFilter = curl/*
    
    compilerOpts.linux = -I/usr/include -I/usr/include/x86_64-linux-gnu
    linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
    linkerOpts.linux = -L/usr/lib/x86_64-linux-gnu -lcurl
    • headers는 Kotlin 스텁을 생성할 헤더 파일 목록입니다. 여기에 여러 파일을 공백으로 구분하여 추가할 수 있습니다. 이 경우 curl.h만 해당됩니다. 참조된 파일은 지정된 경로(이 경우 /usr/include/curl)에 있어야 합니다.
    • headerFilter는 정확히 무엇이 포함되는지 보여줍니다. C에서는 하나의 파일이 #include 지시어를 사용하여 다른 파일을 참조할 때 모든 헤더도 포함됩니다. 때로는 필요하지 않은 경우도 있으며, glob 패턴을 사용하여 이 매개변수를 추가하여 조정할 수 있습니다.

    headerFilter는 (시스템 stdint.h 헤더와 같은) 외부 의존성을 상호 운용 라이브러리로 가져오고 싶지 않을 때 사용할 수 있습니다. 또한 라이브러리 크기 최적화 및 시스템과 제공된 Kotlin/Native 컴파일 환경 간의 잠재적 충돌을 해결하는 데 유용할 수 있습니다.

    • 특정 플랫폼에 대한 동작을 수정해야 하는 경우, compilerOpts.osx 또는 compilerOpts.linux와 같은 형식을 사용하여 옵션에 플랫폼별 값을 제공할 수 있습니다. 이 경우 macOS(.osx 접미사) 및 Linux(.linux 접미사)에 해당합니다. 접미사가 없는 매개변수(예: linkerOpts=)도 가능하며 모든 플랫폼에 적용됩니다.

    사용 가능한 옵션의 전체 목록은 정의 파일을 참조하십시오.

샘플이 작동하려면 시스템에 curl 라이브러리 바이너리가 있어야 합니다. macOS 및 Linux에서는 일반적으로 포함되어 있습니다. Windows에서는 소스에서 빌드할 수 있습니다(Microsoft Visual Studio 또는 Windows SDK 커맨드라인 도구가 필요합니다). 자세한 내용은 관련 블로그 게시물을 참조하십시오. 또는 MinGW/MSYS2 curl 바이너리를 고려할 수도 있습니다.

빌드 프로세스에 상호 운용성 추가

헤더 파일을 사용하려면 빌드 프로세스의 일부로 생성되도록 해야 합니다. 이를 위해 build.gradle.kts 파일에 다음 compilations {} 블록을 추가합니다.

kotlin
nativeTarget.apply {
    compilations.getByName("main") {
        cinterops {
            val libcurl by creating
        }
    }
    binaries {
        executable {
            entryPoint = "main"
        }
    }
}

먼저 cinterops가 추가되고, 그 다음 정의 파일에 대한 항목이 추가됩니다. 기본적으로 파일 이름이 사용됩니다. 추가 매개변수로 이를 재정의할 수 있습니다.

kotlin
cinterops {
    val libcurl by creating {
        definitionFile.set(project.file("src/nativeInterop/cinterop/libcurl.def"))
        packageName("com.jetbrains.handson.http")
        compilerOpts("-I/path")
        includeDirs.allHeaders("path")
    }
}

애플리케이션 코드 작성

이제 라이브러리와 해당 Kotlin 스텁이 있으므로 애플리케이션에서 이를 사용할 수 있습니다. 이 튜토리얼에서는 simple.c 예제를 Kotlin으로 변환합니다.

src/nativeMain/kotlin/ 폴더에서 Main.kt 파일을 다음 코드로 업데이트합니다.

kotlin
import kotlinx.cinterop.*
import libcurl.*

@OptIn(ExperimentalForeignApi::class)
fun main(args: Array<String>) {
    val curl = curl_easy_init()
    if (curl != null) {
        curl_easy_setopt(curl, CURLOPT_URL, "https://example.com")
        curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)
        val res = curl_easy_perform(curl)
        if (res != CURLE_OK) {
            println("curl_easy_perform() failed ${curl_easy_strerror(res)?.toKString()}")
        }
        curl_easy_cleanup(curl)
    }
}

보시다시피, Kotlin 버전에서는 명시적인 변수 선언이 제거되었지만, 그 외의 모든 것은 C 버전과 거의 동일합니다. libcurl 라이브러리에서 예상할 수 있는 모든 호출은 Kotlin에서도 동일하게 사용할 수 있습니다.

이것은 한 줄씩 직역한 것입니다. 더 Kotlin스러운 방식으로 작성할 수도 있습니다.

애플리케이션 컴파일 및 실행

  1. 애플리케이션을 컴파일합니다. 이를 위해 작업 목록에서 runDebugExecutableNative Gradle 작업을 실행하거나 터미널에서 다음 명령을 사용합니다.

    bash
    ./gradlew runDebugExecutableNative

    이 경우 cinterop 도구에 의해 생성된 부분이 빌드에 암묵적으로 포함됩니다.

  2. 컴파일 중 오류가 없으면 main() 함수 옆의 여백에 있는 녹색 Run 아이콘을 클릭하거나 / 단축키를 사용합니다.

    IntelliJ IDEA가 Run 탭을 열고 example.com의 내용을 출력으로 보여줍니다.

    Application output with HTML-code

curl_easy_perform 호출이 결과를 표준 출력으로 인쇄하기 때문에 실제 출력을 볼 수 있습니다. curl_easy_setopt를 사용하여 이를 숨길 수 있습니다.

전체 프로젝트 코드는 GitHub 저장소에서 얻을 수 있습니다.

다음 단계

Kotlin의 C 상호 운용성에 대해 자세히 알아보세요.