C interop와 libcurl을 사용한 앱 만들기 – 튜토리얼

C interop와 libcurl을 사용한 앱 만들기 – 튜토리얼

C 라이브러리 임포트는 Beta 단계예요. cinterop 도구가 C 라이브러리에서 생성하는 모든 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 Multiplatform 플러그인을 통해 Gradle 빌드 시스템을 사용할 수 있어요.

출처: Create an app using C interop and libcurl – tutorial

본문

시작하기 전에

  • 최신 버전의 IntelliJ IDEA를 다운로드하고 설치하세요.
  • IntelliJ IDEA에서 File | New | Project from Version Control을 선택하고 다음 URL을 사용해 프로젝트 템플릿을 클론하세요:
https://github.com/Kotlin/kmp-native-wizard
  • 프로젝트 구조를 살펴보세요:

Native application project structure

이 템플릿에는 시작하는 데 필요한 파일과 폴더가 있는 프로젝트가 포함되어 있어요. Kotlin/Native로 작성된 애플리케이션은 코드에 플랫폼별 요구 사항이 없다면 다른 플랫폼을 타깃으로 할 수 있다는 점을 이해하는 게 중요해요. 코드는 nativeMain 디렉터리에 있고, 그에 대응하는 nativeTest가 있어요. 이 튜토리얼에서는 폴더 구조를 그대로 유지하세요.

  • 프로젝트 설정이 담긴 빌드 스크립트 build.gradle.kts 파일을 열어 보세요. 빌드 파일에서 다음 부분에 특히 주의하세요:
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

kotlin {
    macosArm64()
    linuxArm64()
    linuxX64()
    mingwX64()

    targets.withType<KotlinNativeTarget>().configureEach {
        binaries {
            executable {
                entryPoint = "main"
            }
        }
    }
}
  • 타깃은 macOS, Linux, Windows용으로 macosArm64, linuxArm64, linuxX64, mingwX64를 사용해 정의해요. 지원되는 플랫폼의 전체 목록을 확인하세요.
  • binaries {} 블록은 바이너리가 생성되는 방식과 애플리케이션의 진입점을 정의해요. 이 값들은 기본값으로 두면 돼요.
  • C 상호 운용은 빌드의 추가 단계로 구성돼요. 기본적으로 C의 모든 심볼은 interop 패키지로 임포트돼요. .kt 파일에서 전체 패키지를 임포트하고 싶을 수도 있어요. 구성 방법에 대해 더 알아보세요.

정의 파일 만들기

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

Kotlin/Native는 표준 C 라이브러리를 소비하는 데 도움을 주며, 여러분이 필요로 하는 거의 모든 것에 존재하는 방대한 기능 생태계를 열어 줘요. Kotlin/Native에는 이미 미리 빌드된 플랫폼 라이브러리 세트가 함께 제공되어 표준 라이브러리에 몇 가지 추가적인 공통 기능을 제공해요.

interop의 이상적인 시나리오는 C 함수를 같은 시그니처와 규칙을 따르는 Kotlin 함수처럼 호출하는 거예요. 이때 cinterop 도구가 유용해져요. cinterop은 C 라이브러리를 받아 그에 대응하는 Kotlin 바인딩을 생성해서, 그 라이브러리를 마치 Kotlin 코드인 것처럼 사용할 수 있게 해 줘요.

이 바인딩들을 생성하려면 각 라이브러리에 보통 라이브러리와 같은 이름의 정의 파일이 필요해요. 이 정의 파일은 라이브러리를 정확히 어떻게 소비해야 하는지 설명하는 프로퍼티 파일이에요.

이 앱에서는 HTTP 호출을 위해 libcurl 라이브러리가 필요해요. 정의 파일을 만들려면:

  • src 폴더를 선택하고 File | New | Directory로 새 디렉터리를 만들어요.
  • 새 디렉터리 이름을 nativeInterop/cinterop으로 지어요. 이는 헤더 파일 위치의 기본 규칙이지만, 다른 위치를 사용한다면 build.gradle.kts 파일에서 재정의할 수 있어요.
  • 이 새 하위 폴더를 선택하고 File | New | File로 새 libcurl.def 파일을 만들어요.
  • 파일을 다음 코드로 업데이트해요:
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 헤더)을 interop 라이브러리로 가져오고 싶지 않을 때 사용할 수 있어요. 또한 라이브러리 크기 최적화와 시스템 및 제공된 Kotlin/Native 컴파일 환경 사이의 잠재적 충돌을 해결하는 데도 유용할 수 있어요.

  • 특정 플랫폼에 대한 동작을 수정해야 한다면 compilerOpts.osxcompilerOpts.linux 같은 형식을 사용해 옵션에 플랫폼별 값을 제공할 수 있어요. 이 경우에는 macOS(.osx 접미사)와 Linux(.linux 접미사)예요. 접미사가 없는 파라미터(예: linkerOpts=)도 가능하며 모든 플랫폼에 적용돼요.

사용 가능한 옵션의 전체 목록은 정의 파일 문서를 참고하세요.

샘플이 작동하려면 시스템에 curl 라이브러리 바이너리가 있어야 해요. macOS와 Linux에서는 보통 포함되어 있어요. Windows에서는 소스에서 빌드할 수 있어요(Microsoft Visual Studio 또는 Windows SDK 커맨드라인 도구가 필요해요). 자세한 내용은 관련 블로그 글을 참고하세요. 또는 MinGW/MSYS2 curl 바이너리를 고려할 수도 있어요.

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

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

targets.withType<KotlinNativeTarget>().configureEach {
    compilations.getByName("main") {
        cinterops {
            val libcurl by creating
        }
    }
    binaries {
        executable {
            entryPoint = "main"
        }
    }
}

먼저 cinterops를 추가하고, 그 다음 정의 파일에 대한 항목을 추가해요. 기본적으로 파일의 이름이 사용돼요. 추가 파라미터로 재정의할 수 있어요:

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 파일을 다음 코드로 업데이트하세요:

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다운 관용적인 방식으로 작성할 수도 있어요.

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

  • 애플리케이션을 컴파일하려면 작업 목록에서 runDebugExecutable<YourTargetName> Gradle 작업을 실행하거나 터미널에서 콘솔 명령을 사용해요. 예:
./gradlew runDebugExecutableMacosArm64

이 경우 cinterop 도구가 생성한 부분이 빌드에 암시적으로 포함돼요.

  • 컴파일 중에 오류가 없다면 main() 함수 옆의 여백(gutter)에 있는 초록색 Run 아이콘을 클릭하거나 Shift + Cmd + R/Shift + F10 단축키를 사용하세요.

IntelliJ IDEA가 Run 탭을 열고 출력——example.com의 내용——을 보여 줘요:

Application output with HTML-code

curl_easy_perform 호출이 결과를 표준 출력으로 인쇄하기 때문에 실제 출력을 볼 수 있어요. curl_easy_setopt를 사용해 이를 숨길 수도 있어요.

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

다음 단계

Kotlin의 C와의 상호 운용에 대해 더 알아보세요.

더 알아보기

  • Kotlin/Native를 동적 라이브러리로 사용하기 – 튜토리얼
  • 정의 파일(Definition file)