Define dynamic C interoperability
Archived MVP specification. For ASPL 0.5.0, see the current reference and native HTTP guide.
Type: grilling Status: resolved Blocked by: 02, 05, 06, 07 Parent: Find the ASPL MVP specification
Question
What source declaration syntax, shared-library naming and lookup rules, supported C ABI types and signatures, symbol resolution behavior, lifetime rules, unsafe boundary, and error behavior should make the claim that an ASPL program can call a compatible Linux shared library accurate and testable?
Answer
The x86-64 glibc MVP uses explicit foreign declarations, a closed catalog of two exact C function types, and eager POSIX dynamic loading before main. Source selects a named foreign adapter profile. The runtime never infers a C type from an ASPL signature and never calls through an approximate function-pointer type.
The detailed C and ABI basis remains in Runtime C calls from a pure-C register VM.
Source declarations and calls
The normalized grammar adds these forms:
foreign-declaration =
UNSAFE FOREIGN identifier "(" [ parameters ] ")" "->" type
"=" adapter-profile "(" string-literal "," string-literal ")" terminator ;
foreign-call = UNSAFE identifier "(" [ arguments ] ")" terminator ;
The two string literals name the shared library and C symbol in that order. A declaration gives the foreign function an ordinary ASPL local name and an explicit ASPL-facing signature. That name shares the top-level callable namespace with ASPL functions and other foreign declarations. C symbols, library strings, and paths are never translated.
For example:
unsafe foreign print_line(message: string) -> i64 = c_int__const_char_ptr("libc.so.6", "puts");
غير_آمن خارجي اطبع_سطر(رسالة: نص) -> ص64 = سي_صحيح__مؤشر_محرف_ثابت("libc.so.6", "puts").
Each profile has one canonical Arabic name and one exact English alias. Alias normalization follows the keyword rules, and both spellings are reserved:
| Arabic name | English alias | Bytecode adapter ID |
|---|---|---|
سي_صحيح__مؤشر_محرف_ثابت |
c_int__const_char_ptr |
1 |
سي_مزدوج__مزدوج |
c_double__double |
2 |
The double underscore separates the C result type from the ordered C parameter types. Bytecode stores only the adapter ID. An unknown ID makes bytecode invalid, and changing an ID or profile meaning requires a new bytecode version.
The checker requires the declaration's ASPL parameter and result types to match its profile exactly. It also requires unsafe on the declaration and as a prefix on every call:
let result = unsafe print_line("مرحبا");
The Arabic غير_آمن form is equivalent. There is no unsafe block, implicit foreign call, function value, or way to obtain the resolved function address as c_ptr. Source unsafe markers are checked before bytecode generation and are not serialized.
Adapter catalog and conversions
Version 1 has exactly two profiles:
| ID | Exact C function type | ASPL-facing signature |
|---|---|---|
| 1 | int (*)(const char *) |
(string) -> i64 |
| 2 | double (*)(double) |
(f64) -> f64 |
Profile 1 creates a call-scoped C-string loan. Before entering C, the adapter rejects an ASPL string containing NUL, allocates a copy with room for one terminator, copies the exact bytes, and appends \0. It releases the copy when the function returns. A compatible function may read this pointer during the call but may not modify or retain it. The returned C int converts by value to i64; negative results remain ordinary negative integers.
Profile 2 passes and returns an IEEE 754 binary64 value. The adapter canonicalizes a returned NaN according to the runtime value model. It also restores ASPL's required floating-point environment after the foreign function returns so changes made by C cannot affect later ASPL arithmetic.
Neither profile observes errno, floating exception flags, or library-specific error conventions. The return value is ordinary ASPL data. The catalog contains no variadic, aggregate, callback, data-symbol, C object-pointer, or other scalar signature. A later release may add a reviewed profile only with a real compatible function and conformance tests.
Library and symbol rules
Library and symbol names must be nonempty UTF-8 string literals without embedded NUL. ASPL passes the library bytes unchanged to dlopen. It does not add a lib prefix, append .so, select a version, expand a project search path, or otherwise rewrite the name.
A bare name uses the glibc dynamic loader's normal search rules. A name containing / uses the loader's path rules. This makes the runtime environment, including its loader configuration and installed libraries, part of deployment rather than build validation.
aspl check and aspl build validate the declaration and adapter profile without opening the library. Native execution prepares all imports before entering main:
- The compiler orders imports by library bytes, symbol bytes, then adapter ID.
- The runtime processes the serialized import table in that order.
- It opens each exact library string at most once with
RTLD_NOW | RTLD_LOCAL. - It resolves the named symbol with
dlsymon that library's handle, using thedlerrorprotocol so a null address is not mistaken for lookup failure. - It converts the resolved address only to the profile's exact precompiled C function-pointer type.
The source checker rejects two foreign declarations with the same library and symbol byte strings, even when their local names or profiles differ. Lookup never falls back to RTLD_DEFAULT, and the MVP has no GNU symbol-version selector.
If preparation fails, the runtime stops at the first import-table failure, closes successfully opened handles in reverse opening order, and never calls main. After successful preparation, handles and resolved function pointers remain valid until no VM instance can use that prepared program. A standalone native process may leave their final release to process exit.
Pointer and foreign lifetime rules
The initial catalog has no profile that creates or consumes a non-null c_ptr. The type remains ready for later exact profiles without implying that all C object-pointer types are interchangeable.
For any later approved pointer profile, the adapter converts only its declared exact C object-pointer type to or from ASPL's opaque storage. Null becomes that type's null pointer. ASPL does not track a pointer's C type, provenance, ownership, or lifetime. Ordinary code may relay and compare it, but the programmer must pass only a pointer accepted by the target API while its pointee remains alive. Cleanup requires an explicit compatible foreign call. The VM never frees a pointee or runs language-level cleanup during a trap.
A function pointer is never an ASPL pointer. Calling C with the wrong pointer provenance, using a dangling pointer, or violating a library's ownership contract is misuse of an unsafe call and lies outside ASPL's trap guarantees.
Failure behavior
The source checker rejects these cases:
- an unknown profile name or an ASPL signature that does not match the selected profile;
- an empty or NUL-containing library or symbol string;
- duplicate declarations for the same library and symbol bytes;
- a missing unsafe marker on either the declaration or a call; and
- every declaration outside the two-profile catalog.
The bytecode loader independently rejects unknown adapter IDs, invalid import strings, incompatible call registers, and malformed import records as load failures.
Dynamic preparation adds two stable load-failure reasons:
foreign-library-open-failed, with the import index and library string; andforeign-symbol-lookup-failed, with the import index, library string, and symbol string.
Unstable host text from dlerror may accompany either reason but is not part of its identity. Diagnostics and CLI behavior own localized wording and process exit codes.
Creating a C-string loan from a value containing NUL produces the ASPL trap foreign-string-contains-nul at the current function and instruction. Allocation failure while creating the copy remains a VM or host failure.
ASPL cannot turn arbitrary foreign behavior into traps. A C function may crash, terminate or stall the process, corrupt memory, retain a call-scoped string, return an invalid pointer, or violate its declared type. Those cases lie outside ASPL's guarantees. The browser host does not load Linux shared libraries or execute bytecode containing foreign imports.
Conformance evidence
The MVP conformance suite must:
- call real
putsfromlibc.so.6through adapter 1; - call real
sqrtfromlibm.so.6through adapter 2; - build a test-only shared library with the same two exact signatures and use it to verify negative
intconversion, canonical NaN return, floating-point environment restoration, and loading of another compatible library; - reject every declaration error listed above in an independent fixture;
- prove that checking and building do not require a declared library to exist;
- prove that a missing library or symbol prevents every
mainside effect; - prove that embedded NUL traps before control enters C; and
- validate equivalent malformed imports from standalone bytecode so raw input cannot bypass source checks.
The test library is a contributor and release-test asset, not an installed ASPL runtime dependency.
Compatibility claim
Documentation and release material must use this complete claim:
On x86-64 Linux with glibc, an ASPL native program can load any shared library accepted by the glibc dynamic loader and call a named function when its exact C type matches a supported foreign adapter profile. The MVP supports
int(const char *)anddouble(double). Library availability, pointer validity, and foreign-function behavior remain the program's responsibility.
The project must not shorten this to "call arbitrary C functions," "full C compatibility," or "C header compatibility."