thisisthepy
Ecosystem생태계 / Projects프로젝트 / python-multiplatform
Language and runtime

python-multiplatform

CPython embedded in Kotlin Multiplatform, with Kotlin and Python calling each other in both directions.Kotlin Multiplatform 안에 CPython을 넣고, Kotlin과 Python이 서로를 양방향으로 부르게 합니다.

GitHub · Guide

Overview개요

python-multiplatform is the bridge the rest of the ecosystem stands on. It embeds an unmodified CPython in a Kotlin Multiplatform app on Android, iOS, desktop and the web, and lets Kotlin and Python call each other as if they were one program. It also carries the Compose host that draws a Python-declared UI, and TypedPython, the compiler for typed Python.

python-multiplatform은 생태계의 나머지가 올라서는 다리입니다. 고치지 않은 CPython을 Android, iOS, 데스크톱, 웹의 Kotlin Multiplatform 앱에 넣고, Kotlin과 Python이 한 프로그램처럼 서로를 부르게 합니다. Python이 선언한 UI를 그리는 Compose 호스트와, 타입 붙은 Python의 컴파일러인 TypedPython도 여기에 있습니다.

The problem it solves푸는 문제

Earlier ways of putting Python in an app chose one platform (Chaquopy on Android), one direction (calling Python from a host, not the reverse), or one hand-written wrapper per API. None of them lets Python use any Kotlin library, on every Kotlin platform, without someone writing glue for it first.

앱에 Python을 넣던 이전 방법들은 한 플랫폼(Android의 Chaquopy), 한 방향(호스트가 Python을 부르기만 함), 또는 API마다 손으로 쓴 래퍼 중 하나를 골랐습니다. 누군가 먼저 접착 코드를 쓰지 않아도 Python이 모든 Kotlin 플랫폼에서 아무 Kotlin 라이브러리나 쓰게 해 주는 방법은 없었습니다.

How it works동작 방식

Downcalls: Kotlin to Python

Every CPython C API function the bridge uses, about 330 of them, is one expect declaration in common code, with an actual per platform: natives registered through JNI's RegisterNatives on Android (choosing the calling convention per function and per API level), the Foreign Function and Memory API with invokeExact on desktop, direct Kotlin/Native calls on iOS, and WebAssembly imports on the web. A typed object model sits on top, rooted at PyObject, and every wrapper states whether it owns or borrows its reference.

Upcalls: Python to Kotlin

Python reaches Kotlin through a function table generated at build time. For the app's own Kotlin, a KSP processor generates it. For any library, Compose included, the Gradle plugin's artifact walker reads the compiled jars and klibs and generates Kotlin source that kotlinc compiles, so the compiler, not a name lookup, picks every method, including the mangled names of value-class functions. Python enters Kotlin through one entry point per platform.

The binder

Kotlin packages are importable from Python under their own names. Inside a module, functions, classes, properties and keyword arguments are also reachable in snake_case. Overloads are chosen the way Kotlin would choose them, down to the type of an integer literal, and value classes cross as the Kotlin objects they are. The Gradle plugin writes .pyi stubs for everything, so editors and type checkers see the real signatures.

The Compose host

PythonAppView, PythonWidget and PythonLauncher draw a Python-declared root inside a Compose UI; a redeclared root replaces the screen. rememberSaveableWrapper gives Python Compose's saved state, which survives rotation and process death.

downcall: Kotlin에서 Python으로

다리가 쓰는 CPython C API 함수 약 330개가 공통 코드의 expect 선언 하나씩이고, 플랫폼마다 actual이 있습니다. Android는 JNI RegisterNatives로 등록한 네이티브이고 함수와 API 레벨마다 호출 규약을 고릅니다. 데스크톱은 invokeExact를 쓰는 Foreign Function and Memory API, iOS는 Kotlin/Native 직접 호출, 웹은 WebAssembly import입니다. 그 위에 PyObject를 뿌리로 하는 타입 있는 객체 모델이 있고, 모든 래퍼가 참조를 소유하는지 빌리는지 밝힙니다.

upcall: Python에서 Kotlin으로

Python은 빌드 시점에 생성한 함수 표로 Kotlin에 닿습니다. 앱 자신의 Kotlin은 KSP 프로세서가 생성합니다. Compose를 포함한 모든 라이브러리는 Gradle 플러그인의 아티팩트 워커가 컴파일된 jar와 klib을 읽고 Kotlin 소스를 생성하면 kotlinc가 컴파일합니다. 그래서 value class 함수의 변형된 이름까지 모든 메서드를 이름 검색이 아니라 컴파일러가 고릅니다. Python은 플랫폼마다 진입점 하나로 Kotlin에 들어갑니다.

바인더

Kotlin 패키지를 원래 이름 그대로 Python에서 import합니다. 모듈 안의 함수, 클래스, 속성, 키워드 인자는 snake_case로도 부를 수 있습니다. 오버로드는 정수 리터럴의 타입까지 Kotlin과 같은 방식으로 고르고, value class는 원래의 Kotlin 객체로 건넙니다. Gradle 플러그인이 모두의 .pyi 스텁을 써서 에디터와 타입 검사기가 진짜 시그니처를 봅니다.

Compose 호스트

PythonAppView, PythonWidget, PythonLauncher가 Python이 선언한 루트를 Compose UI 안에 그리고, 루트를 다시 선언하면 화면이 바뀝니다. rememberSaveableWrapper는 Python에 화면 회전과 프로세스 종료 뒤에도 살아남는 Compose의 저장 상태를 줍니다.

Design decisions설계 결정

  • No runtime reflection, no name lookup. Kotlin/Native and GraalVM native images allow neither, so every binding is generated at build time.
  • Never rename a namespace. androidx stays androidx; snake_case aliases exist only inside modules.
  • Exposure is a blacklist. Every public declaration is bound; @PythonInternal opts out.
  • Platform types only where the platform has them. JVM classes, Kotlin classes and Objective-C classes are not stubbed into uniformity.
  • Parallelism from free-threaded CPython, not sub-interpreters, whose extension compatibility rules out most of the ecosystem.
  • Upcalls must work in a GraalVM native image. A feature that passes only on the JVM is not done.
  • 실행 중 리플렉션 없음, 이름 검색 없음. Kotlin/Native와 GraalVM 네이티브 이미지는 둘 다 허용하지 않아서, 모든 바인딩을 빌드할 때 생성합니다.
  • 네임스페이스 이름을 바꾸지 않음. androidx는 androidx이고, snake_case 별칭은 모듈 안에만 있습니다.
  • 노출은 블랙리스트. 모든 public 선언을 바인딩하고, @PythonInternal로 뺍니다.
  • 플랫폼 타입은 그 플랫폼에만. JVM 클래스, Kotlin 클래스, Objective-C 클래스를 억지로 똑같이 흉내 내지 않습니다.
  • 병렬성은 free-threaded CPython에서. 서브 인터프리터는 확장 호환성 때문에 생태계 대부분을 쓸 수 없게 해서 택하지 않았습니다.
  • upcall은 GraalVM 네이티브 이미지에서도 돌아야 합니다. JVM에서만 통과하는 기능은 끝난 것이 아닙니다.

Where it fits생태계 안의 위치

Below it: CPython, provided per platform by the toolchain and pypackpack. Above it: every pythonx package, TypedPython's compiled modules, torchnative inside apps, and the apps themselves. Its Gradle plugin is used by toolchain to stage interpreters and generate bindings.

아래에는 toolchain과 pypackpack이 플랫폼마다 제공하는 CPython이 있습니다. 위에는 모든 pythonx 패키지, TypedPython의 컴파일 모듈, 앱 안의 torchnative, 그리고 앱이 있습니다. toolchain은 이 저장소의 Gradle 플러그인으로 인터프리터를 준비하고 바인딩을 생성합니다.

Where it stands현재 상태

  • Working on desktop, Android and iOS: downcalls, upcalls, proxies, async upcalls, the artifact walker over real Compose jars, stubs, the Compose host.
  • Verified by pythonx-compose's lecture notebook running end to end from an installed wheel: 20 scenarios pixel-equal to the same UI written in Kotlin, including Korean input-method composition with no Python call per keystroke.
  • Verified on the iOS simulator: the app bundle carries the standard library with extension modules wrapped as frameworks and the app's Python payload.
  • Experimental: the web target.
  • 동작 데스크톱, Android, iOS: downcall, upcall, 프록시, 비동기 upcall, 실제 Compose jar에 대한 아티팩트 워커, 스텁, Compose 호스트.
  • 검증 pythonx-compose 강의 노트북이 설치된 wheel로 처음부터 끝까지 돕니다. 시나리오 20개가 Kotlin으로 쓴 같은 UI와 픽셀까지 같고, 한글 입력기 조합도 키마다 Python 호출 없이 동작합니다.
  • 검증 iOS 시뮬레이터: 앱 번들에 표준 라이브러리(확장 모듈은 프레임워크로 감쌈)와 앱의 Python 코드가 들어갑니다.
  • 실험 웹 타깃.

Future design미래 설계

  • Free-threaded only. CPython 3.15t with no GIL on every platform, built by us for Android, iOS and WebAssembly, using the free-threaded stable ABI (abi3t). The web runs under cross-origin isolation by default so browser threads are real threads.
  • Python on Kotlin's threads. A Python coroutine stepped by a Kotlin coroutine on any dispatcher, JVM virtual threads included, which pythonx-concurrent builds on.
  • The whole Kotlin ecosystem, automatically. Any library on the classpath becomes a Python module with stubs, at build time.
  • free-threaded 전용. 모든 플랫폼에서 GIL 없는 CPython 3.15t를 쓰고, Android, iOS, WebAssembly용은 직접 빌드하며, free-threaded 안정 ABI(abi3t)를 씁니다. 웹은 기본으로 교차 출처 격리 아래에서 돌아 브라우저 스레드가 진짜 스레드가 됩니다.
  • Kotlin 스레드 위의 Python. Kotlin 코루틴이 어떤 디스패처에서든, JVM 가상 스레드에서도 Python 코루틴을 한 단계씩 실행합니다. pythonx-concurrent가 이 위에 섭니다.
  • Kotlin 생태계 전체가 자동으로. 클래스패스의 모든 라이브러리가 빌드할 때 스텁과 함께 Python 모듈이 됩니다.

Roadmap로드맵

Next milestone 2026-11-30: free-threaded 3.15t, the consumer iOS wiring in the Gradle plugin, the TypedPython build gate by default, and compiled modules shipped through pypackpack.

다음 마일스톤 2026-11-30: free-threaded 3.15t, Gradle 플러그인의 외부 앱용 iOS 연결, 기본으로 켜진 TypedPython 빌드 게이트, pypackpack으로 배포되는 컴파일 모듈.