main() { printf("Hello, %s", "world!") } #include <stdio.h> int main() { printf("Hello, %s\n", "world!") } C 표준 함수의 printf를 그대로 호출 가능 실행: konanc main.kt –o main && ./main.kexe
main() { printf("Hello, %s", "world!") } #include <stdio.h> int main() { printf("Hello, %s\n", "world!") } C 표준 함수의 printf를 그대로 호출 가능 실행: konanc main.kt –o main && ./main.kexe
함수의 선언: 함수의 시그니처만 포함 • 함수의 정의: 시그니처와 body를 둘 다 포함 • Java에서 .java 파일 하나에 .class 파일 하나가 나오는 것처럼 (nested class가 없는 경우) C에서도 .c 파일 하나에 .o (윈도 VC++는 .obj) 파일 하나가 생성됨 • 함수 호출 → .o 파일에 그 함수를 쓴다는 표시만 하고, 실제 함수 내용물은 포함 x • 컴파일 결과들을 모아 최종적인 실행 파일로 만드는 ʻ링크’ 과정을 거쳐야 함 • .o 파일이 누락되면 undefined symbols error가 발생 • Java에서 .class 파일이 누락되면 NoClassDefFoundError가 발생하는 것과 비슷 cinterop으로 klib에 정적 아카이브 포함시키기
my.h my_add() Kotlin IR my_add() my.c와 my.o는 my_add()가 무슨 일을 하는 함수인지 알지만 my.h와 my.klib, main.kt는 my_add()의 존재만 알고 무슨 일을 하는 함수인지 모름 my.klib my_add() main.ll main() my_add() + main.kexe main() my_add() Kotlin IR main() my_add() main.kt main() my_add() 만약 여기서 libmy.a를 넣지 않는다면? → my_add()의 내용물이 없으므로 링커 오류 .a 파일 = .o 파일을 여러개 담을 수 있는 파일
my.h my_add() Kotlin IR my_add() my.klib my_add() main.ll main() my_add() + main.kexe main() my_add() Kotlin IR main() my_add() main.kt main() my_add() my.klib가 libmy.a를 포함하고 있으므로 링커 오류가 발생하지 않음
Foo = objc_getClass("Foo"); SEL new_selector = NSSelectorFromString(@"new"); id foo = objc_msgSend(Foo, new_selector); SEL setValue_selector = NSSelectorFromString(@"setValue"); objc_msgSend(foo, setValue_selector, 5); • Objective-C 전용 문법은 C로 만들어진 Objective-C 런타임 함수 호출로 번역됨 • C 함수 호출이 가능하다면 어떤 언어에서든 Objective-C 클래스 생성/호출 가능 * Selector는 컴파일 시 별도 테이블이 생성되므로, 아래 코드보다 위 코드의 속도가 더 빠름. 아래 코드는 일종의 비유로, 완전히 같은 기계어를 생성하는 코드가 아님.
쪽이 서로 호환되 도록 하는 규약 • 호출 규약: 인자와 반환값을 어떤 방식으로 전달하는가 (레지스터 또는 스택) • 메모리 alignment: 각 타입이 램에 저장되어있을 때, 크기와 주솟값은 어떤 값을 가 져야하는가 • 이름 mangling: 코드 상에서의 함수 명이 기계어 상에선 어떻게 변화하는가 • 규약만 맞추면 서로 다른 언어에서 서로를 호출 가능 • C ABI만 맞추면 Kotlin/Native와 Obj-C에서 서로를 호출 가능 ABI: Application Binary Interface * Objective-C의 ABI는 기본적으로 C ABI를 따르고, exception을 처리하기 위한 Itanium C++ ABI를 추가로 사용함.
증가함 • 객체를 더 이상 참조하지 않게 되면 RC가 1 감소함 • RC가 0인 객체는 자동으로 소멸자(destructor) 호출 • Destructor가 호출되는 시점이 deterministic하다. ARC: Automatic Reference Counting * Destructor와 finalizer (Mark-and-sweep GC가 달린 언어에서 사용되는 그 개념) 모두 소멸자로 번역되니 잘 구분할 필요가 있다.
안드로이드에 android.view.View가 있다면 iOS에는 UIView와 UIViewController가 있다. • UIView와 UIViewController 모두 Objective-C 인터페이스 § SwiftUI도 iOS에서 내부적으로 UIKit 을 활용 § Flutter도 내부적으로 FlutterViewController라는 인터페이스가 있음 § React Native도 기술 특성 상 iOS에서 여러가지 UIView의 서브클래스를 구현하고 있음 § CMP도 ComposeHostingViewController라는 서브클래스를 구현하고 있음 → 이것이 fun ComposeUIViewController()이 반환하는 클래스 UIKit: iOS의 핵심 UI API
여러 파일이 들어있는 구조 • Info.plist: 앱의 여러 메타데이터를 담는 파일 (AndroidManifest.xml과 비슷) • 유닉스 실행 파일: main() 함수를 담고 있는 기계어 파일 • _CodeSignature: 앱 서명 • embedded.mobileprovision: 앱을 정해진 기기에 설치할 수 있게 authorize해주는 파일 • 유닉스 실행 파일을 순수 Kotlin으로 만들 수 있다!
• Flutter도 Impeller라는 자체 그래픽스 엔진을 만들기 전엔 Skia를 사용했었음 • Android가 아닌 플랫폼에서도 Kotlin으로 Skia를 사용하기 위해 JetBrains에서 Skiko라는 라이브러리를 개발 중 • CMP는 Skiko 기반으로 만들어짐 Skia: 2D 그래픽스 라이브러리
• PictureRecorder를 통해 Canvas 객체 가져오기 • CMP에서 Canvas를 통해 렌더 작업 수행 • ComposeHostingViewController가 가지고 있는 CAMetalLayer로부터 Skia 렌더 타겟을 생성 • Canvas에 기록된 렌더 결과를 렌더 타겟에 표시 CMP가 iOS에서 UI를 그리는 과정
- fun render(Canvas, …) ComposeContainerView : UIView UIKitInteropContainer - overlayView: UIView - backgroundView: UIView MetalView : UIView - layer: CAMetalLayer - fun render(SkCanvas, …) UIKitInteropContainer.overlayView UIKitInteropContainer.backgroundView Subviews ComposeScene - fun setContent(@Composable) 실제 렌더링 결과가 그려지는 UIView fun ComposeUIViewController()가 반환하는 것 * Compose Multiplatform 1.10 기준 ** 일부 생략된 클래스 있음
포함하고 있음 • fun <T : UIView> UIKitView(…) • fun <T : UIViewController> UIKitViewController(…) • 삽입된 UIView는 해당 위치에서의 LayoutNode와 항상 같은 크기와 위치를 가지게 됨 • CMP 1.9 이하 기준, 넘겨진 UIView와 UIViewController는 MetalView 아래에 삽입됨 androidx.compose.ui.viewinterop * LayoutNode: Box, Column, Row 같은 친구들
됨 • class UIKitInteropProperties(…, val placedAsOverlay: Boolean) • fun <T> UIKitView(…, UIKitInteropProperties) • fun <T> UIKitViewController(…, UIKitInteropProperties) • placedAsOverlay의 기본값은 false → 기존처럼 아래에 삽입 • true면 기존과 달리 Compose에서 렌더된 결과를 가리게 됨 androidx.compose.ui.viewinterop
-> Unit, private val onImage: (ByteArray?) -> Unit, ) : NSObject(), UINavigationControllerDelegateProtocol, UIImagePickerControllerDelegateProtocol { override fun imagePickerControllerDidCancel(picker: UIImagePickerController) { picker.dismissViewControllerAnimated(flag = true) {} onDismiss() } override fun imagePickerController( picker: UIImagePickerController, didFinishPickingMediaWithInfo: Map<Any?, Any?>, ) { picker.dismissViewControllerAnimated(flag = true) {} val uiImage = didFinishPickingMediaWithInfo[UIImagePickerControllerEditedImage] as? UIImage onImage(uiImage?.toByteArray()) } private fun UIImage.toByteArray(): ByteArray? = ... } 취소 시 onDismiss() 호출 성공 시 이미지를 onImage() 에 전달
객체가 정리되지 않음이 보장 • 약한 참조 → 참조를 들고 있어도 객체가 정리될 수 있다 • 약한 참조는 객체가 살아있는 동안 강한 참조로 변환할 수 있다 • 객체가 이미 정리된 상태에서 강한 참조로 변환을 시도하면 null이 반환됨 • 강한 참조로만 이루어진 사이클이 생기는 경우 ARC에선 메모리 누수 발생 • 메모리 누수를 막기 위해 사이클이 생길만한 곳에서 약한 참조를 사용한다 약한 참조: 참조를 들고 있어도 객체가 정리될 수 있다
(ByteArray?) -> Unit) { val rememberedOnDismiss by rememberUpdatedState(onDismiss) val rememberedOnImage by rememberUpdatedState(onImage) val uiViewController = LocalUIViewController.current DisposableEffect(Unit) { val imagePickerController = UIImagePickerController().apply { sourceType = UIImagePickerControllerSourceTypePhotoLibrary allowsEditing = true delegate = ImagePickerDelegate( { rememberedOnDismiss() }, { rememberedOnImage(it) }, ) } ... } } 이 시점에서 ImagePickerDelegate를 들고 있는 곳 - 약한 참조: UIImagePickerController - 강한 참조: 없음 강한 참조를 들고 있는 곳이 없으므로 delegate = … 가 끝나자마자 ImagePickerDelegate는 정리됨
onDismiss: () -> Unit, private val onImage: (ByteArray?) -> Unit, ) : NSObject(), UINavigationControllerDelegateProtocol, UIImagePickerControllerDelegateProtocol { init { objc_retain(objcPtr()) } override fun imagePickerControllerDidCancel(picker: UIImagePickerController) { picker.dismissViewControllerAnimated(flag = true) {} onDismiss() objc_release(objcPtr()) } override fun imagePickerController( picker: UIImagePickerController, didFinishPickingMediaWithInfo: Map<Any?, Any?>, ) { picker.dismissViewControllerAnimated(flag = true) {} val uiImage = didFinishPickingMediaWithInfo[UIImagePickerControllerEditedImage] as? UIImage onImage(uiImage?.toByteArray()) objc_release(objcPtr()) } Delegate의 참조 카운트를 강제로 1 증가 Objective-C 런타임이 강한 참조가 하나 있다고 착각하게 함 다 쓴 후 참조 카운트를 강제로 1 감소 호출하지 않으면 메모리 누수 발생 다 쓴 후 참조 카운트를 강제로 1 감소 호출하지 않으면 메모리 누수 발생
IR을 생성한다. • 백엔드는 IR을 읽어서 각 플랫폼에 맞는 결과물을 생성한다. • Kotlin IR을 읽어서 LLVM IR을 생성하는 기술이 바로 Kotlin/Native • 생성된 LLVM IR은 LLVM의 기능에 따라 각 운영체제 전용 실행 파일로 빌드된다. Kotlin/Native의 동작 원리
있다. • .klib는 KMP 전용 라이브러리 형식이다. • Kotlin/Native 컴파일러로 .klib 파일을 생성할 수 있다. • cinterop을 통해 C 헤더로부터 .klib 파일을 생성할 수 있다. • cinterop을 통해 정적 아카이브를 .klib 파일 안에 넣을 수 있다. • Kotlin/Native 컴파일러에 .klib 파일을 집어넣으면 다른 .kt 파일에서 라이브러리를 사용할 수 있다. • Kotlin/Native도 기계어를 생성하는 기술이기 때문에, 다른 기술과 마찬가지로 링커 오류가 날 수 있다. Kotlin/Native 컴파일러 커맨드라인에서 사용하기
C 코드만으로 호출할 수 있다. • C ABI만 맞추면 어떤 언어에서든 Objective-C를 사용할 수 있다. • UIKit은 iOS의 핵심 UI API이다. • Kotlin/Native와 UIKit을 사용하면 Swift나 Objective-C 없이 iOS 네이티브 앱을 만들 수 있다. Kotlin/Native + UIKit으로 iOS 앱 만들기
Metal 레이어를 가지는 UIView에 UI를 렌더한다. • CMP는 Metal 레이어 위 아래로 UIView를 추가로 삽입할 수 있는 API를 제공한다. • LocalUIViewController를 통해 화면 전체를 가리는 UIKit API도 사용할 수 있다. • CMP와 UIKit을 연동하면 UI 로직 대부분을 공유하면서도 필요한 곳에선 네이티브 뷰를 사용하는 앱을 쉽게 만들 수 있다. CMP에서 UIKit 잘 연동하기