ADR-062: A Target That Lets Foreign Code Call In Is a Target, and the Boundary Is Where the Safe Shape Is Paid For

Status: Superseded by ADR-284 — its D1-D3 are D1-D3 there Related: ADR-061 D2 (every count plain at one user thread, and the sentence this examines), ADR-037 D1 (the target decides what std offers), ADR-037 D2 (user_parallelism bounds your code), ADR-045 D1 (the verdict never reads the switch), ADR-038 D7 (a foreign runtime brings threads that are not ours)

1. Context

ADR-061 D2 makes every reference count plain at user_parallelism = no, and it rests on one sentence: nothing a user writes can cross a thread there. That sentence is true because the program owns every thread it runs on — main is ours, the runtime’s I/O worker carries no user code (ADR-038 §4.2), and since ADR-053 a Nikaia dependency is a crate of the same build at the same setting.

The roadmap asks for two things that end that ownership: extern "C" (Part III 15.1, specified and unbuilt) and Python bindings. Both turn the program into a library somebody else calls, and the caller brings its own threads.

What this does not bring back is the problem ADR-061 D1 is about. D1 refuses a Shared at a foreign call because a Rust library has one signature and this compiler chooses the count per value. A C signature names neither shape — it names a pointer and an integer and nothing else, so the representation question cannot even be posed there. The exported surface is what C can spell, which the ledger’s writable types already are.

What it does bring back is that the caller owns the threads. An exported entry point may be called twice at once from two threads of the caller’s, and no switch of ours said yes to that. Two of our tasks are then in flight together with the cheap count under them.

The GIL is not the guarantee it is remembered as. A C extension may release it around its own work, and free-threaded CPython removes it outright. A Python target that assumed one caller thread would be assuming something CPython itself has stopped promising.

And this is the first form in which the question can arise at all. A Nikaia library is source today, so its settings are the consumer’s. The question needs an artifact somebody links rather than builds — which this repository has no notion of: there is no cdylib, no staticlib, no .so anywhere in it.

2. Decisions

D1 — Whether foreign code may call in is a property of the target, not a third value of user_parallelism.

The target already decides what std offers and what a panic does (ADR-037 D1), which is the same kind of question: what is outside this program, and what does it do to us. user_parallelism bounds your code (ADR-037 D2 — the word user is the whole of it) and therefore cannot answer for code that is not yours. Asking it to would make it mean two things at once, and the second one silently.

So a build that exports entry points is a target of its own. x86_64-linux is a program that owns its threads; a target that produces a linkable artifact is one that does not, and it says so in the place where the answer is already kept.

D2 — One build, safe at its boundary. Not two artifacts, and not the safe shape everywhere.

Every exported entry point, and everything reachable from one, takes the safe count and the safe lock. Everything else keeps the per-value answer (ADR-037 D7).

The two alternatives are worse in the same way — they both pay everywhere for a property only the boundary has:

The analysis already answers per value. An entry point is a root, and a root seeded at the floor is the whole of this: what it reaches is atomic because it reaches it, and what nothing reaches from outside stays cheap.

D3 — The checks do not change, because no check ever read the switch.

This is the payoff of ADR-045 D1 and ADR-061 D3, and it is why the decision is small. contracts::send asks nothing about user_parallelism — the crossing verdict is about a type and a destination. A contract in the ledger is written at the floor whatever the build says. So a library with foreign entry points is already checked for the world it is about to enter; only what is emitted moves, and only under its entry points.

3. Consequences

A Nikaia library does not have to ship twice. The question that opened this — whether a binary library needs a single-threaded and a thread-safe build — has the answer no, and it has it without giving up the per-value count inside.

The switch keeps its meaning at the one place it could have lost it. no goes on saying nothing you wrote runs twice at once, because on a target where that could be false the target says so first.

What it costs: the analysis needs the set of exported entry points as roots, which it does not have, because there is no way to export one yet. That is the same order of work as the crossing roots it already seeds.

4. What this does not decide

5. What is built

Nothing, and nothing can be. extern "C" is a parse error (Part III 15.1), Target has two values and one of them is refused with a reason, and the repository has no notion of a linkable artifact. The decision is recorded now because it is the first direction that touches user_parallelism at its root, and a target added without it would answer the question by accident.

This site is open source. Improve this page.