Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 32 additions & 32 deletions AGENTS.md

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions PROJECT.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# python-multiplatform — 프로젝트 요점
# python-multiplatform, 프로젝트 요점

Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로의 라이브러리를 양방향으로**
쓰게 한다. 모든 지원 플랫폼에서 같은 방식으로 동작하고, 경계를 넘는 비용을 측정하며 줄인다.
Expand Down Expand Up @@ -35,7 +35,7 @@ Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로
| 플랫폼 | Kotlin 실행 환경 | 다운콜 | 업콜 진입 | 상태 |
|---|---|---|---|---|
| Desktop (macOS) | JVM | Panama FFM (`invokeExact`) | FFM upcall stub | 전체 스위트 · GraalVM 네이티브 이미지 업콜 검증 |
| Desktop (Linux/Windows) | JVM | 동상 | 동상 | 배선만 — 실행 기록 없음 |
| Desktop (Linux/Windows) | JVM | 동상 | 동상 | 배선만, 실행 기록 없음 |
| Android | ART | JNI `RegisterNatives` | JNI | 기기 테스트 |
| iOS | Kotlin/Native | cinterop | `@CName` | 시뮬레이터에서 공통 스위트 |
| androidNative | Kotlin/Native | cinterop | `@CName` | 기기에서 공통 스위트 |
Expand All @@ -45,7 +45,7 @@ Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로

| 경로 | 내용 |
|---|---|
| `python-multiplatform/` | 라이브러리 — FFI 계층(`python/native/ffi/`), 객체 모델(`python/multiplatform/ffi/`), 업콜 런타임 |
| `python-multiplatform/` | 라이브러리, FFI 계층(`python/native/ffi/`), 객체 모델(`python/multiplatform/ffi/`), 업콜 런타임 |
| `python-multiplatform-ksp/` | 업콜 테이블을 생성하는 KSP 프로세서 |
| `python-multiplatform-gradle-plugin/` | `stagePythonHome`, 아티팩트 워커(빌드된 jar/klib 바인딩), `.pyi` 스텁 생성 |
| `ksp-fixtures/` | 생성기 산출물을 실제로 쓰는 소비자 모듈 (`app`, `compose`, `artifact`, …) |
Expand All @@ -67,7 +67,7 @@ Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로
```

- 컴파일 확인은 세 타깃: `compileKotlinAndroidNativeArm64`(기본 루프), `compileKotlinIosSimulatorArm64`,
`compileKotlinDesktop`. **androidNative 를 빼먹지 않는다** — `nativeMain` 을 iOS 와 공유한다.
`compileKotlinDesktop`. **androidNative 를 빼먹지 않는다**, `nativeMain` 을 iOS 와 공유한다.
- 생성기 소비자 검증은 **모듈마다 따로**: `:ksp-fixtures:app:desktopTest`,
`:ksp-fixtures:compose:desktopTest`, `:ksp-fixtures:artifact:desktopTest`.
- 결과는 `build/test-results/<target>/*.xml` 을 지우고 `--rerun` 으로 다시 돌려 센다.
Expand All @@ -84,15 +84,15 @@ Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로
KSP 업콜 테이블과 프록시(클래스·프로퍼티·companion·`suspend`·취소), jar 아티팩트 워커, Python 에서
Compose 렌더(데스크톱), Android 부트스트랩, GraalVM 네이티브 이미지 업콜(데스크톱, 수동 검증).
- **부분**: iOS·androidNative·wasm 의 업콜과 수명 검증, klib 워커(스캐너만), `.pyi` 스텁, GIL 해제,
Linux/Windows, free-threaded 는 desktop 한정·옵트인(3.14t, `-PpythonFreeThreaded=true`, 236 테스트 0 실패 — ROADMAP §9).
Linux/Windows, free-threaded 는 desktop 한정·옵트인(3.14t, `-PpythonFreeThreaded=true`, 236 테스트 0 실패, ROADMAP §9).
- **계획**: Android/iOS 의 free-threaded(프리빌트 없음), Android/iOS/wasm 의 Compose, 네이티브 이미지 검증 자동화.

## 6. 큰 결정들

| 결정 | 근거 |
|---|---|
| Python→Kotlin 은 **런타임 리플렉션 아님** — 빌드 타임 테이블 | Kotlin/Native 에 리플렉션이 사실상 없고, GraalVM 네이티브 이미지는 closed-world 다 |
| **JVM 메서드를 이름으로 찾지 않는다** — Kotlin 소스를 생성해 `kotlinc` 가 컴파일 | 값 클래스 맹글링 접미사가 겹쳐서 이름 조회는 원리적으로 불가능하다 |
| Python→Kotlin 은 **런타임 리플렉션 아님**, 빌드 타임 테이블 | Kotlin/Native 에 리플렉션이 사실상 없고, GraalVM 네이티브 이미지는 closed-world 다 |
| **JVM 메서드를 이름으로 찾지 않는다**, Kotlin 소스를 생성해 `kotlinc` 가 컴파일 | 값 클래스 맹글링 접미사가 겹쳐서 이름 조회는 원리적으로 불가능하다 |
| 바인더는 **Kotlin 네임스페이스를 다른 이름으로 내보내지 않는다** | 사용자 규정. `androidx.*` 는 원본 Kotlin, `pythonx.*` 는 pythonx-compose 의 실제 패키지 |
| 노출은 **블랙리스트** (`@PythonInternal`) | 화이트리스트는 붙이기를 잊으면 조용히 사라진다 |
| 병렬성은 **free-threading**, 멀티 인터프리터 아님 | 인터프리터별 GIL 은 C 확장이 `Py_mod_multiple_interpreters` 를 선언해야 import 된다 |
Expand All @@ -103,11 +103,11 @@ Kotlin Multiplatform 에 CPython 을 임베딩해 **Kotlin 과 Python 이 서로

## 7. 열린 질문

`docs/SPEC.md` 끝의 "Outside intent — needs a decision" 이 원본이다. 요지:
`docs/SPEC.md` 끝의 "Outside intent, needs a decision" 이 원본이다. 요지:

1. `.pyi` 스텁 생성기가 매니페스트 없이도 `androidx.*` 를 `pythonx.*` 로 바꿔 쓴다 — 네임스페이스
1. `.pyi` 스텁 생성기가 매니페스트 없이도 `androidx.*` 를 `pythonx.*` 로 바꿔 쓴다, 네임스페이스
개명 금지 규정과 충돌하는가.
2. 라이브러리가 `sys.modules` 에 합성 `pythonx` 모듈(`__path__ = []`)을 넣는다 — 실제 `pythonx`
2. 라이브러리가 `sys.modules` 에 합성 `pythonx` 모듈(`__path__ = []`)을 넣는다, 실제 `pythonx`
패키지를 가리는가.
3. 멤버 이름의 snake_case 변환은 의도된 것인가.
4. 범용 `pythonx` 어댑터가 이 저장소에 있어야 하는가, pythonx-compose 에 있어야 하는가.
Expand Down
34 changes: 17 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ English | [한국어](docs/locale/README_ko.md)

# 🐍 python-multiplatform

**Real CPython inside Kotlin Multiplatform — Kotlin calls Python, Python calls Kotlin, on every target.**
**Real CPython inside Kotlin Multiplatform, Kotlin calls Python, Python calls Kotlin, on every target.**

[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Kotlin Multiplatform](https://img.shields.io/badge/Kotlin-Multiplatform-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org/docs/multiplatform.html)
Expand All @@ -21,31 +21,31 @@ English | [한국어](docs/locale/README_ko.md)

## 💡 Why

Python has the libraries; Kotlin Multiplatform has the reach — desktop, Android, iOS, the web.
Python has the libraries; Kotlin Multiplatform has the reach, desktop, Android, iOS, the web.
python-multiplatform puts **the real CPython interpreter** inside a Kotlin Multiplatform app and
builds a two-way bridge across it:

- Kotlin gets a **typed, Kotlin-idiomatic object model** over Python: a `PyList` *is* a `MutableList`,
a `PyDict` *is* a `MutableMap`, a Python error *is* a `Throwable`.
- Python gets **Kotlin classes as ordinary Python**: `Greeter('Kotlin').greet(2)`, `await` on a
`suspend fun`, Kotlin extension functions as methods — all through a table generated at build time,
`suspend fun`, Kotlin extension functions as methods, all through a table generated at build time,
so it works where reflection does not: Kotlin/Native and GraalVM native images.

It is CPython itself, not a re-implementation, so C extensions keep working.

## ✨ Features

- 🔌 **Downcalls** — the CPython Stable ABI (~330 functions) as one `expect` surface with an `actual`
- 🔌 **Downcalls**: the CPython Stable ABI (~330 functions) as one `expect` surface with an `actual`
per platform: Panama `invokeExact` on desktop, `RegisterNatives` JNI on Android, cinterop on Native.
- 🧩 **Object model** — `PyObject`, `PyType`, `PyException`, basic types, `PyList` / `PyDict` /
- 🧩 **Object model**: `PyObject`, `PyType`, `PyException`, basic types, `PyList` / `PyDict` /
`PySet` / `PyTuple` implementing Kotlin collection interfaces, modules, callables, conversion.
- 🚀 **Upcalls** — a KSP processor generates a function table; Python constructs Kotlin classes, calls
- 🚀 **Upcalls**: a KSP processor generates a function table; Python constructs Kotlin classes, calls
methods, reads and writes properties, and `await`s `suspend` functions with cancellation.
- 📦 **Bind prebuilt libraries** — the Gradle plugin walks jars (Compose included) and generates the
- 📦 **Bind prebuilt libraries**: the Gradle plugin walks jars (Compose included) and generates the
bindings and `.pyi` stubs, with default-argument omission and value-class support.
- ♻️ **No manual memory management** — wrappers release their Python reference when collected; reference
- ♻️ **No manual memory management**: wrappers release their Python reference when collected; reference
cycles that cross the boundary are collected by Python's cyclic GC.
- 🧪 **Measured** — every boundary mechanism ships with overhead tests; the cost table is rendered from
- 🧪 **Measured**: every boundary mechanism ships with overhead tests; the cost table is rendered from
real runs ([cost table](docs/investigations/cost-table.md)).

## 🚀 Quick start
Expand Down Expand Up @@ -118,7 +118,7 @@ name, unchanged.
### Platform setup

<details>
<summary><b>Desktop</b> — apply the Gradle plugin so <code>PYTHONHOME</code> has a stdlib</summary>
<summary><b>Desktop</b>, apply the Gradle plugin so <code>PYTHONHOME</code> has a stdlib</summary>

```kotlin
plugins {
Expand All @@ -138,7 +138,7 @@ prefix with `-Dpython.multiplatform.home=...`. See `docs/platforms/desktop-packa
</details>

<details>
<summary><b>Android</b> — call <code>PythonBootstrap.initialize</code></summary>
<summary><b>Android</b>, call <code>PythonBootstrap.initialize</code></summary>

```kotlin
import python.multiplatform.env.PythonBootstrap
Expand Down Expand Up @@ -172,7 +172,7 @@ flowchart LR
T --> K
```

Behind each platform's single entry point, **every platform uses the same generated table** — there is
Behind each platform's single entry point, **every platform uses the same generated table**, there is
no per-platform Python → Kotlin binding code. Details: [design notes](docs/design/).

## 🌍 Platforms
Expand All @@ -188,7 +188,7 @@ no per-platform Python → Kotlin binding code. Details: [design notes](docs/des

## 📊 Status

Honest summary — see the [roadmap](docs/roadmap/ROADMAP.md) for what is left to do.
Honest summary, see the [roadmap](docs/roadmap/ROADMAP.md) for what is left to do.

| Area | State |
|---|---|
Expand All @@ -213,10 +213,10 @@ Honest summary — see the [roadmap](docs/roadmap/ROADMAP.md) for what is left t

## 📖 Documentation

- 🌐 **[Guide](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어) — source in [`docs/guide/`](docs/guide/)
- 🏗 [Design notes](docs/design/) — architecture, downcalls, upcalls, lifetime, threading, binding policy
- 📱 [Platform notes](docs/platforms/) — Android FFM, wasm, CPython acquisition, GraalVM verification
- 🔬 [Investigations](docs/investigations/) — measurements and audits
- 🌐 **[Guide](https://thisisthepy.github.io/python-multiplatform/)** (English / 한국어), source in [`docs/guide/`](docs/guide/)
- 🏗 [Design notes](docs/design/), architecture, downcalls, upcalls, lifetime, threading, binding policy
- 📱 [Platform notes](docs/platforms/), Android FFM, wasm, CPython acquisition, GraalVM verification
- 🔬 [Investigations](docs/investigations/), measurements and audits
- 🗺 [Roadmap](docs/roadmap/ROADMAP.md)
- 🇰🇷 [README in Korean](docs/locale/README_ko.md)

Expand Down
30 changes: 15 additions & 15 deletions benchmarks/cost_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ def _samples(runs: list[dict[str, Any]], target: str, source: str, key: str,

def _fmt_range(values: list[float], unit: str) -> str:
if not values:
return "—"
return ", "
lo, hi = min(values), max(values)
suffix = "x" if unit == "x" else " ns"
if abs(hi - lo) < 0.005:
Expand Down Expand Up @@ -780,7 +780,7 @@ def render_markdown(report: dict[str, Any]) -> str:
targets = report.get("targetsRequested") or DEFAULT_TARGETS
lines: list[str] = []

dirty = " **(working tree dirty — these numbers are not reproducible from the commit alone)**" \
dirty = " **(working tree dirty, these numbers are not reproducible from the commit alone)**" \
if report.get("workingTreeDirty") else ""
lines.append(f"_Generated by `benchmarks/cost_table.py` from "
f"`{report.get('sourceFile', 'the newest JSON')}`._")
Expand Down Expand Up @@ -815,31 +815,31 @@ def render_markdown(report: dict[str, Any]) -> str:
host = report["host"]
warm = report.get("benchmarkTestWarmup", {})
lines += [
f"- **commit** `{report['commit'][:12]}` — {report.get('commitSubject', '')}{dirty}",
f"- **commit** `{report['commit'][:12]}`, {report.get('commitSubject', '')}{dirty}",
f"- **branch** `{report.get('branch', '?')}`, worktree `{report.get('worktree', '?')}`",
f"- **captured** {report['capturedAt']}"
+ (" — the first of several cuts; each one's own time is in the source table below"
+ (", the first of several cuts; each one's own time is in the source table below"
if len(report.get("sources") or []) > 1 else ""),
f"- **host** {host.get('cpu', '?')}, {host.get('cores', '?')} cores, "
f"{host.get('os', '?')} {host.get('machine', '')}",
f"- **load average** {report.get('loadAtStart')} at start, {report.get('loadAtEnd')} at end "
f"(ceiling {report['loadCeiling']}, guard `{report.get('loadGuard')}`)"
+ (" — of the first cut only; per-cut figures below"
+ (", of the first cut only; per-cut figures below"
if len(report.get("sources") or []) > 1 else ""),
f"- **runs per target** {report['runsRequested']} — ranges below are min–max over them",
f"- **runs per target** {report['runsRequested']}, ranges below are min–max over them",
f"- **`UpcallBoundaryCostTest`** warmup and iteration counts are self-reported per target "
"in the table below (the test prints them)",
f"- **`BenchmarkTest`** warmup, read from the source at this commit: "
f"`WARMUP` = {warm.get('WARMUP', '?')}, `WARMUP_BULK` = {warm.get('WARMUP_BULK', '?')} "
"(the ≥4096-char string rows use the second — see that file's own caveat)",
"(the ≥4096-char string rows use the second, see that file's own caveat)",
"- **suite scope** full task, never `--tests`-filtered: narrowing the suite changes what is "
"being measured",
"",
]

sources = report.get("sources") or []
if len(sources) > 1:
lines += ["This table is merged from several cuts — which target came from which:", ""]
lines += ["This table is merged from several cuts, which target came from which:", ""]
lines += _table(
["cut", "commit", "captured", "load at start", "contaminated", "targets"],
[[f"`{s['file']}`", f"`{s['commit']}`", str(s.get("capturedAt")),
Expand Down Expand Up @@ -875,16 +875,16 @@ def render_markdown(report: dict[str, Any]) -> str:
device = sim
wall = "/".join(str(r.get("wallSeconds", "?")) for r in mine)
counts = (f"{suite.get('tests', '?')}/{suite.get('failures', '?')}/{suite.get('skipped', '?')}"
if suite else "—")
if suite else ", ")
rows.append([
f"`{name}`",
f"`{target.task}`" if target else "?",
status if not reason else f"**{status}**",
device or ("—" if not (target and target.avd) else "—"),
platform_name or "—",
device or (", " if not (target and target.avd) else ", "),
platform_name or ", ",
counts,
wall or "—",
conditions or "—",
wall or ", ",
conditions or ", ",
])
lines += _table(
["target", "task", "status", "device / simulator", "platform, self-reported",
Expand All @@ -895,7 +895,7 @@ def render_markdown(report: dict[str, Any]) -> str:
notes = [(n, r) for n, r in notes if r]
if notes:
lines += ["**Targets that did not produce numbers, and why:**", ""]
lines += [f"- `{n}` — {r}" for n, r in notes]
lines += [f"- `{n}`, {r}" for n, r in notes]
lines.append("")

# ---- upcall --------------------------------------------------------------------
Expand All @@ -913,7 +913,7 @@ def render_markdown(report: dict[str, Any]) -> str:
lines += _table(["target"] + [c for c, _ in UPCALL_COLUMNS], rows)
lines.append("")

lines += ["Controls, from the same block — these are what say whether a row moved because the "
lines += ["Controls, from the same block, these are what say whether a row moved because the "
"boundary moved or because the whole host did:", ""]
rows = []
for name in targets:
Expand Down
Loading
Loading