# Manual Jni

> Regenerate the manual JNI surface report via `scripts/manual-jni-report.cs`. Use to answer which bindings remain hand-written (raw `JNIEnv.*` calls, suspend bridges, Java Callable Wrappers), what blocks moving a hand-written helper to the `[ComposeBridge]` generator, or after adding/removing manual JNI (e.g. a new `[ComposeBridge]` migration, a new suspend bridge, a new `[Register("net/compose/…")]` JCW).

- Skill: `jonathanpeppers/manual-jni` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jonathanpeppers/manual-jni`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jonathanpeppers/manual-jni/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: jonathanpeppers (https://skillmd.com/u/jonathanpeppers)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jonathanpeppers/manual-jni

---


# Manual JNI surface

Regenerate `docs/manual-jni.md` — the inventory of every place in
`src/Microsoft.AndroidX.Compose/` that still uses **hand-written JNI**
(`JNIEnv.*` calls, `[Register("net/compose/…")]` JCWs) instead of the
source-generated `[ComposeBridge]` / `[ComposeFacade]` machinery. The
report is produced by a single .NET 10 file-based program at
`scripts/manual-jni-report.cs`. It scans every `.cs` file under
`src/Microsoft.AndroidX.Compose/`, anchors each JNI call to its enclosing
member, pulls a "Why not generated?" rationale from existing XML doc
`<remarks>` and adjacent `// ...` comments, and writes a markdown
report with summary metrics, per-file member tables, and a JCW
catalogue.

## Workflow

### 1. Run the script

From the repo root (Windows / PowerShell):

```pwsh
dotnet run scripts/manual-jni-report.cs
```

That's it — no `.csproj`, no `#:package`, no MSBuild target. Pure
text/regex scan; runs in a couple of seconds.

Expected tail output:

```
Files scanned:           255
Files with manual JNI:   47
JNIEnv.* call sites:     196
JCW classes:             25 (4 with JNI, 21 pure-managed)
[ComposeBridge] partials: 202
[ComposeFacade] partials: 95
Report written: docs/manual-jni.md
```

### 2. Verify the diff

```pwsh
git diff --stat docs/manual-jni.md
```

The headline numbers live in the **Summary** section near the top.
Quick sanity check:

```pwsh
Select-String -Path docs/manual-jni.md -Pattern '^- \*\*' | Select-Object -First 7
```

Re-running the script with no source change should produce a byte-
identical file modulo the `Generated by ... on YYYY-MM-DD HH:MM UTC`
timestamp. If it doesn't, see **Troubleshooting**.

### 3. Commit the regenerated report alongside the change that prompted it

The report is checked in deliberately so reviewers can see the manual
surface shrink (or grow) in the PR diff. Don't commit the script run
on its own — pair it with the bridge migration, new suspend bridge,
or whatever change motivated the refresh.

```pwsh
git add docs/manual-jni.md
git commit --amend --no-edit  # or a fresh commit, depending on context
```

## What the report contains

- **Summary** — files scanned / flagged, raw `JNIEnv.*` call count,
  total lines inside flagged members, JCW totals (split: with-JNI vs
  pure-managed delegate adapters), generator-emitted partials
  (`[ComposeBridge]` + `[ComposeFacade]`), manual : generated ratio.
- **Per-file detail** — one section per `.cs` file containing manual
  JNI or a JCW. Each section opens with the file's `<summary>` /
  leading comment as **Purpose**, notes how many `[ComposeBridge]` /
  `[ComposeFacade]` partials in the same file are excluded, then
  enumerates manual entry points in a table: member name, kind,
  line range, JNI call count, "Why not generated?", "Migration".
- **JCW classes** — every `[Register("net/compose/…")]` class in the
  project, with its Kotlin interface, source location, and whether
  the JCW body itself contains raw JNI.
- **Caveats** — heuristic limitations (regex-based detection,
  comment-driven rationale extraction).

## Improving "Why not generated?" entries

The script extracts the rationale paragraph by walking upward from
the member declaration (up to 30 lines), matching in this order:

1. An explicit **Why raw JNI** / **Why manual** marker — either form
   wins regardless of which comment style hosts it:
   - `// Why raw JNI: …` / `// Why manual: …` line.
   - `///` XML doc `<remarks>` paragraph containing _Why raw JNI_.
2. Otherwise, the adjacent `///`-doc or `//` comment block sitting
   directly above the declaration is used verbatim.
3. Falls back to **TODO: document** if there are no comments above
   the member.

If a member shows **TODO: document** in the table, that's a docs gap
in the source itself. The fix is in the source file, not the script
— add a `// Why raw JNI: ...` line above the method declaration, or
flesh out the existing XML `<remarks>`. Re-run the script and the
entry refreshes.

Don't add a banner *just* to please the report — only document
members where the rationale is non-obvious from the surrounding
context.

## Troubleshooting

**Member detection misses a multi-line declaration** — the script
expects member declarations to begin with the standard modifier
prefix (`public|internal|private|protected|static|...`). If a method
uses an unusual modifier order or layout (`[MethodImpl(...)]` first,
then modifiers on the next line), the regex may skip it and the
member's JNI calls land in a synthetic `(file-level)` row. Either
restructure the declaration to put modifiers on the same line as the
return type, or extend the modifier soup in `rxMethod` /
`rxCtor` near the top of the script.

**JCW count looks wrong** — only Microsoft.AndroidX.Compose's own `[Register("net/compose/…")]`
classes are counted. JCWs Mono.Android emits for upstream interfaces
are deliberately excluded. If a new file registers a JCW under a
different namespace prefix, update the `rxRegister` regex.

**Generator partials counted as manual** — the script excludes
members decorated with `[ComposeBridge]` / `[ComposeFacade]` /
`[ComposeDefaults]` by walking attribute lines immediately above the
declaration. If a comment line splits an attribute from its target,
the exclusion silently fails. Move the attribute adjacent to the
declaration.

**Report regen produced spurious diffs** — every run except for the
header timestamp should be byte-identical. Common causes if it
isn't:

- A non-ASCII char drift (smart quotes, em-dashes) in a source
  comment got mangled by some other tool.
- Member detection picked up a new declaration that wasn't there
  before. To find which entry shifted, note the line range in the
  diffed row (e.g. `Lines 221–246`) and use
  `git diff -U0 src/Microsoft.AndroidX.Compose/<file>.cs | rg -n '^@@'` to
  locate the corresponding source change.

If diffs persist with no source change, the script has a bug; look
at the order-sensitive sites (`OrderBy(j.ClassName, StringComparer.Ordinal)`
on the JCW table, `OrderBy(StartLine)` inside per-file entries).

## Don't do these things

- Don't hand-edit `docs/manual-jni.md`. It's regenerated every run
  and your edit will be lost.
- Don't run the script from inside a subdirectory; the output path
  (`docs/manual-jni.md`) and source-root path
  (`src/Microsoft.AndroidX.Compose`) are relative to the repo root.
- Don't add a `// Why raw JNI:` banner to a member that already has
  a clear `<summary>` / `<remarks>` block — the script's fallback
  reads adjacent doc comments fine.
- Don't include managed helpers with zero `JNIEnv.*` calls in the
  manual tally just because they live in `ComposeBridges.cs`. The
  report tracks _raw JNI usage_, not _everything that isn't
  generator-decorated_; a one-line managed helper isn't relevant
  surface.

