Lina is a tiny shared-memory ABI translator for embedded interpreters.
The core idea is deliberately small:
- C owns one raw shared arena.
- Lina publishes named descriptors for regions inside that arena.
- Python, Julia, JS, Lua, Java, etc. build their own native views over the same bytes.
signal/waitis the control plane; descriptors + raw memory are the data plane.
Lina should not become a new NumPy, a new Julia Array implementation, or a universal object runtime. It should translate enough information for each language runtime to do the right native thing.
This prototype supports:
- shared arena allocation/registration;
- named descriptors;
- dtypes:
u8i64f64
ndim <= 8;- shape and byte strides;
- C-order and Fortran-order stride computation;
- zero-copy Python
memoryview; - Julia
unsafe_wrapdemo code; - existing
signal/wait/try_wait/clearsynchronization.
A Lina descriptor describes bytes, not language objects.
process address space
┌─────────────────────────────────────────────┐
│ C host │
│ │
│ shared arena │
│ base = 0x10000000 │
│ │
│ + offset 0x0000: input payload │◄──── Python memoryview / NumPy view
│ + offset 0x0080: result payload │◄──── Julia unsafe_wrap Array
│ │
│ Python heap: small wrapper object │
│ Julia heap: small wrapper object │
└─────────────────────────────────────────────┘
The payload is not copied. Python and Julia have different wrapper objects, but their data pointer can refer to the same C-owned memory.
The core descriptor is:
#define LINA_MAX_NAME 64
#define LINA_MAX_DIMS 8
typedef struct lina_desc_t {
char name[LINA_MAX_NAME];
uint64_t offset;
uint64_t nbytes;
uint64_t capacity_bytes;
uint32_t dtype;
uint32_t ndim;
uint64_t shape[LINA_MAX_DIMS];
int64_t strides[LINA_MAX_DIMS];
uint32_t layout;
uint32_t generation;
uint32_t flags;
uint32_t reserved;
} lina_desc_t;Important fields:
name: stable cross-language name, like"input"or"result".offset: byte offset fromlina_shared_base().nbytes: logical visible payload size.capacity_bytes: reserved payload size.dtype:LINA_DTYPE_U8,LINA_DTYPE_I64,LINA_DTYPE_F64.shape: dimensions.strides: byte strides.generation: descriptor version; useful when a region is replaced.
void lina_init(void);
void lina_shutdown(void);
void lina_register_shared_memory(void* ptr, size_t bytes);
int lina_alloc_shared_memory(size_t bytes);
void* lina_shared_base(void);
size_t lina_shared_size(void);
void lina_signal(const char* name);
void lina_wait(const char* name);
int lina_try_wait(const char* name);
void lina_clear(const char* name);
int lina_create_array(const char* name, uint32_t dtype,
uint32_t ndim, const uint64_t* shape,
uint32_t layout, lina_desc_t* out);
int lina_open(const char* name, lina_desc_t* out);
int lina_resolve_ptr(const lina_desc_t* desc, void** out_ptr);
int lina_list(lina_desc_t* out, size_t cap, size_t* written);The Python binding intentionally returns a byte-level memoryview. Higher-level code can cast it or wrap it with NumPy.
import lina
lina.init_arena(1024 * 1024)
lina.create_array('input', 'f64', (16,))
x = lina.buffer('input').cast('d')
for i in range(len(x)):
x[i] = float(i + 1)
print(lina.open_desc('input'))With NumPy, the same memoryview can become an ndarray without copying:
import numpy as np
mv = lina.buffer('input')
desc = lina.open_desc('input')
x = np.ndarray(shape=desc['shape'], dtype=np.float64, buffer=mv, strides=desc['strides'])In Julia, the natural bridge is unsafe_wrap:
base = ccall((:lina_shared_base, "liblina_core"), Ptr{UInt8}, ())
desc = lina_open_desc("input")
ptr = base + UInt(desc.offset)
len = Int(desc.nbytes ÷ 8)
A = unsafe_wrap(Array, Ptr{Float64}(ptr), len; own=false)own=false is essential: Julia does not own or free Lina's memory.
All future language adapters follow the same recipe:
lina_open(name, &desc)lina_resolve_ptr(&desc, &ptr)- create the language-native external view:
- JS: external
ArrayBuffer+TypedArray - Lua:
userdataholding pointer + descriptor - Java: JNI
NewDirectByteBufferor Foreign Memory API
- JS: external
Example Java shape:
ByteBuffer bb = Lina.openBuffer("input");
bb.order(ByteOrder.nativeOrder());
DoubleBuffer db = bb.asDoubleBuffer();Example JS shape:
const x = lina.typedArray("input"); // Float64Array over external backing store
x[0] = 42.0;Install Meson and Ninja, then:
meson setup build
meson compile -C buildFor the full embedded Python + Julia demo, Julia development headers and libjulia must be visible to the compiler/linker.
Some Julia installations do not provide julia.pc. In that case, pass flags manually, for example:
meson setup build \
-Djulia=enabled \
-Djulia_include_dir="/path/to/julia/include/julia" \
-Djulia_lib_dir="/path/to/julia/lib"
meson compile -C buildIf you only want the core library, C smoke test, and Python extension:
meson setup build -Djulia=disabled
meson compile -C build
./build/lina_c_smoke
PYTHONPATH=build python examples/standalone_python.pyThe full host demo does this:
- C host allocates a Lina arena.
- C creates two shared arrays:
inputandresult. - Python opens
input, writes Float64 values, and signalsdata_ready. - Julia waits for
data_ready, opens the same memory, doubles the values intoresult, and signalsmath_done. - Python waits for
math_doneand printsresult.
No payload copy is required between Python and Julia.
These rules keep the ABI sane:
- A published region must not be moved while another runtime may hold a view.
- Resize should be implemented as republish/new generation, not raw
realloc()under live views. - Language wrappers must not free Lina-owned memory.
signal/waitshould be used as ownership handoff or phase barrier.- Multidimensional layout must be explicit via
strides, because Python and Julia disagree by default: NumPy commonly uses C-order, Julia uses column-major order.
Lina is the translator, not the owner of language semantics.
It answers:
- where are the bytes?
- what are they called?
- how should a runtime interpret them?
- when is it safe to read/write?
Then each interpreter builds the most natural local object over those bytes.
