ENIF pointer interoperability
Copy MarkdownBeaver uses MLIR's upstream Ptr dialect as the IR-level pointer abstraction. This keeps pointer intent visible until the LLVM boundary without treating a BEAM resource as if it were an SSA value.
Three representations
The interop boundary has three distinct representations:
- A
Beaver.Native.OpaquePtror anotherBeaver.Nativeresource is a host-side owner or handle. It is managed by the BEAM/NIF runtime and does not belong in MLIR IR. - A
!ptr.ptrvalue is a native pointer inside MLIR. ENIF pointer signatures use!ptr.ptr<#ptr.generic_space>soconvert-to-llvmcan map the generic ABI pointer to LLVM address space 0. - A memref carries pointer metadata and ownership-independent shape information. It should stay a memref until an external function actually requires a C pointer.
These representations must cross explicit operations or runtime boundaries; they are not interchangeable casts.
ENIF out-parameters
Allocate typed storage as a memref, then use ptr.to_ptr at the call boundary:
alias Beaver.MLIR.Dialect.{MemRef, Ptr}
generic_space = Ptr.generic_space()
ptr_type = Ptr.type()
storage =
MemRef.alloca(operand_segment_sizes: :infer) >>>
Type.memref!([], Type.i64(ctx: ctx), memory_space: generic_space)
argument = Ptr.to_ptr(storage) >>> ptr_type
ENIF.get_int64(env, term, argument) >>> :infer
value = MemRef.load(storage) >>> Type.i64()The memref owns the storage semantics. ptr.to_ptr exposes only its aligned
data pointer to the ENIF call. Do not reconstruct a memref from a raw ENIF
pointer unless the pointer's metadata, lifetime, and ownership contract are
available explicitly.
Null and raw addresses
Use typed Ptr attributes rather than routing through an LLVM zero and an
unrealized_conversion_cast:
ptr_type = Ptr.type()
null = Ptr.constant(value: Ptr.null(type: ptr_type)) >>> ptr_type
address = Ptr.constant(value: Ptr.address(0x1000, type: ptr_type)) >>> ptr_typeAn address constant does not acquire ownership or extend a lifetime. It is valid only when the native side guarantees that the address is meaningful for the complete use interval.
Loads, stores, arithmetic, and address spaces
For values that are already native pointers, use upstream ptr.load,
ptr.store, ptr.ptr_add, and ptr.ptr_diff. Vector pointer values can use
the upstream masked load/store and gather/scatter operations.
When Ptr operations remain inside an llvm.func until LLVM IR translation,
make the target address space explicit:
generic_llvm_pointer = Ptr.type(memory_space: {:llvm, 0})
shared_llvm_pointer = Ptr.type(memory_space: {:llvm, 3})This distinction makes lowering inspectable:
ENIF/memref ABI path:
memref<..., #ptr.generic_space>
-> ptr.to_ptr
-> !ptr.ptr<#ptr.generic_space>
-> convert-to-llvm
-> !llvm.ptr (address space 0)
Direct LLVM translation path:
!ptr.ptr<#llvm.address_space<N>>
-> MLIR LLVM translation
-> LLVM ptr addrspace(N)Resource handles and ERL_NIF_TERM values are not raw addresses. Keep them in
their declared integer/resource representation and cross into native pointer
IR only through an API whose ownership and lifetime contract is explicit.