Problem / motivation
Appendix B Q6 in docs/ROADMAP.md asks how
Brainrot owns native resources:
Ownership of native resources. Textures, sockets, and map entries all
outlive statements. Brainrot has no destructors and no GC. Handles sidestep
this by keeping ownership in C — is that the general answer?
It is now the single largest constraint on the FFI, and it is measurable.
STDROT_STRUCT (#208, ABI v3) carries an aggregate into a native by value,
but a struct return is rejected outright at analysis time, because
returning one needs an ownership answer this ABI has not chosen. The roadmap's
own sketch for the generated binding is not implementable as written for
exactly that reason — stdrot_struct("Texture2D", &tex, sizeof(tex)) returns
the address of a local that dies with the call, and STDROT_STRING's
"deep-copy immediately" trick cannot rescue it, since that trick relies on the
borrowed storage still being alive at the moment of return.
The cost, from the generator's own report:
Skipped by rayrot-gen |
Count |
| struct returns |
113 |
handle-like struct params (Image, Font, Model, Sound, Shader) |
102 |
| structs with pointer/array fields |
19 |
const char * returns |
14 |
| callback params |
7 |
| varargs |
2 |
113 of the 239 skipped raylib functions — more than the other five categories
combined — and the 102 handle-like params are the same question wearing a
different hat. Answering Q6 is worth more to this binding than every other open
item put together.
Proposed solution
This needs a decision before it needs code. Three shapes, none obviously
right:
- Handles, ownership stays in C — what Road A already does
(rayrot/raylib.c keeps a Texture2D textures[] and Brainrot holds an
integer index). Proven, and already shipped. But every resource type needs
its own table, handles have no generation counter so a stale index silently
aliases whatever loads into that slot next, and the generator cannot
invent a table per type without being told which types are resources.
- Ownership transfer to the interpreter — a returned aggregate becomes an
interpreter-owned blob freed with the scope. Needs an allocator agreement
across the .so boundary, and an answer for aggregates that own nested
heap memory (Image.data, Font.glyphs) — a byte copy of those is a
double-free waiting to happen.
- Something GC-shaped or refcounted — a language-design project of its
own, and the roadmap does not otherwise commit to one.
Whichever is chosen, the follow-on work is: lift the STDROT_STRUCT return
rejection in semantic_check_native_call(), add the return-side marshalling
marshal_native_return_value() (ast.c) currently has no case for, and teach
rayrot/rayrot_gen.py to emit returning adapters — its skip bookkeeping
already reports exactly which functions become available.
Alternatives considered
Leaving it. That is a defensible answer and is what has happened so far — the
argument direction alone was enough to make the generated binding useful, and
the rejection is loud and documented rather than silent. What it costs is that
Vector2 GetMousePosition(void)-shaped functions, which is most of raylib's
math and query surface, stay unreachable and each needs a hand-written
scalar-flattening wrapper.
Additional context
- Roadmap: Appendix B Q6, and the Phase 5 Road B section which records the
113-function cost.
- The
STDROT_STRUCT comment in stdrot/stdrot_api.h documents precisely why
the return direction was left out and what would have to be true to add it —
worth reading before designing, since it names the failure mode of the
obvious approach.
- Appendix B Q7 (generated code in-tree vs built) was resolved the same
way this one should be: written down as a decision with the rejected
alternatives and why, before any code depended on it.
- Related: the
STDROT_CSTRING return gap and STDROT_HANDLE are the same
question in miniature — both are rejected today for want of an ownership
model, and both would likely fall out of whatever answer Q6 gets.
Not a good first issue. This is an architectural decision with
language-wide consequences, and the code that follows it touches the ABI, the
semantic analyzer, the return marshaller and the generator. It wants a
maintainer's call on direction first; the implementation after that is
substantial but tractable.
Problem / motivation
Appendix B Q6 in
docs/ROADMAP.mdasks howBrainrot owns native resources:
It is now the single largest constraint on the FFI, and it is measurable.
STDROT_STRUCT(#208, ABI v3) carries an aggregate into a native by value,but a struct return is rejected outright at analysis time, because
returning one needs an ownership answer this ABI has not chosen. The roadmap's
own sketch for the generated binding is not implementable as written for
exactly that reason —
stdrot_struct("Texture2D", &tex, sizeof(tex))returnsthe address of a local that dies with the call, and
STDROT_STRING's"deep-copy immediately" trick cannot rescue it, since that trick relies on the
borrowed storage still being alive at the moment of return.
The cost, from the generator's own report:
rayrot-genImage,Font,Model,Sound,Shader)const char *returns113 of the 239 skipped raylib functions — more than the other five categories
combined — and the 102 handle-like params are the same question wearing a
different hat. Answering Q6 is worth more to this binding than every other open
item put together.
Proposed solution
This needs a decision before it needs code. Three shapes, none obviously
right:
(
rayrot/raylib.ckeeps aTexture2D textures[]and Brainrot holds aninteger index). Proven, and already shipped. But every resource type needs
its own table, handles have no generation counter so a stale index silently
aliases whatever loads into that slot next, and the generator cannot
invent a table per type without being told which types are resources.
interpreter-owned blob freed with the scope. Needs an allocator agreement
across the
.soboundary, and an answer for aggregates that own nestedheap memory (
Image.data,Font.glyphs) — a byte copy of those is adouble-free waiting to happen.
own, and the roadmap does not otherwise commit to one.
Whichever is chosen, the follow-on work is: lift the
STDROT_STRUCTreturnrejection in
semantic_check_native_call(), add the return-side marshallingmarshal_native_return_value()(ast.c) currently has no case for, and teachrayrot/rayrot_gen.pyto emit returning adapters — its skip bookkeepingalready reports exactly which functions become available.
Alternatives considered
Leaving it. That is a defensible answer and is what has happened so far — the
argument direction alone was enough to make the generated binding useful, and
the rejection is loud and documented rather than silent. What it costs is that
Vector2 GetMousePosition(void)-shaped functions, which is most of raylib'smath and query surface, stay unreachable and each needs a hand-written
scalar-flattening wrapper.
Additional context
113-function cost.
STDROT_STRUCTcomment instdrot/stdrot_api.hdocuments precisely whythe return direction was left out and what would have to be true to add it —
worth reading before designing, since it names the failure mode of the
obvious approach.
way this one should be: written down as a decision with the rejected
alternatives and why, before any code depended on it.
STDROT_CSTRINGreturn gap andSTDROT_HANDLEare the samequestion in miniature — both are rejected today for want of an ownership
model, and both would likely fall out of whatever answer Q6 gets.
Not a good first issue. This is an architectural decision with
language-wide consequences, and the code that follows it touches the ABI, the
semantic analyzer, the return marshaller and the generator. It wants a
maintainer's call on direction first; the implementation after that is
substantial but tractable.