| title | Pointers |
|---|---|
| description | How prik handles Fortran `pointer` variables, results, fields, and descriptors |
| audience | advanced users |
| prerequisites | arrays, allocatables |
| related | allocatables.md, memory-management.md |
| status | maintained |
| publication | reviewed |
A Fortran pointer describes an association with target storage. The pointer descriptor records whether a target is present and, for arrays, its address, shape, and strides. It does not by itself say who owns that target.
- A pointer descriptor refers to target storage; it does not own that storage by default.
- Scalar pointers appear as
T | None; array pointers use livePointer[T[...]]handles. associateddescribes association, not ownership or target lifetime.- NumPy arrays returned by
to_numpy()are live views, not copies. - Reassociation, resizing, or deallocation can invalidate existing views.
associate(other)makes two pointer handles refer to the same target without copying it.- Use
deallocate()only if this pointer was used to create its current target withallocate(). Otherwise, usenullify(). close()releases a returned or caller-created descriptor, not its target.
Use Pointer[T[...]] when the native callable needs the pointer descriptor:
from prik.contracts import Float64, Pointer
module_values: Pointer[Float64[:]]
def inspect_pointer(values: Pointer[Float64[:]]) -> Float64: ...Use ordinary T[...] when the callable needs only array data:
def sum_values(values: Float64[:]) -> Float64: ...An associated pointer handle may satisfy an ordinary array parameter when its
dtype, rank, shape, layout, and contiguity meet that parameter's contract. A
plain NumPy array cannot satisfy a Pointer[T[...]] parameter because it does
not carry a native pointer descriptor.
Pointer[T[...]] is the type annotation. At runtime, generated Python APIs use
a PointerArray. You can also create an unassociated handle when a routine
needs a present pointer descriptor that it will associate:
import prik.contracts as xc
target = xc.Pointer[xc.Float64[:]]()
assert target.associated is False
api.choose_target(target)
assert target.associated is TrueThe annotation supplies the element dtype and rank. The handle creates its
native descriptor storage when first passed to a matching writable argument.
It stays the same Python object after the call.
Pointer[Float64]() is not supported because scalar pointers cross the Python
boundary as values rather than array handles.
| Member | Type | Behavior |
|---|---|---|
associated |
bool |
Whether the descriptor currently has a target. |
shape |
tuple[int, ...] | None |
Current target dimensions, or None when unassociated. |
dtype |
numpy.dtype |
Declared target element type. |
rank |
int |
Declared number of dimensions. |
to_numpy() |
numpy.ndarray | None |
A live target view, or None when unassociated. |
associate(other) |
(PointerArray) -> None |
Makes this pointer's association match other without copying data. |
nullify() |
() -> None |
Removes the association without destroying the target. |
allocate(shape) |
(int | Sequence[int]) -> None |
Creates and associates a target for an unassociated pointer. |
deallocate() |
() -> None |
Destroys the current target if this pointer was used to allocate it. |
resize(shape) |
(int | Sequence[int]) -> None |
Replaces the current target when deallocate() is valid. |
close() |
() -> None |
Permanently releases returned or caller-created descriptor storage; it does not deallocate the target. It does nothing on a module or field handle. |
closed |
bool |
Whether a closable handle has been closed. |
associate() and nullify() are available by default. A handle may also
support allocation, target deallocation, resizing, and NumPy extraction.
An unavailable operation raises NotImplementedError.
p1 = xc.Pointer[xc.Float64[:]]()
p1.associate(p2)Both pointers must have the same dtype and rank. If p2 is associated, both
pointers refer to the same target. If p2 is unassociated, p1 becomes
unassociated. No data is copied.
Any previous association of p1 is removed without destroying its old target.
If p1 is responsible for a target created with p1.allocate(), call
p1.deallocate() before reassociating it. Otherwise, that memory may be left
without a pointer that can release it.
| Operation | What it releases | Handle afterward |
|---|---|---|
nullify() |
This descriptor's association. It does not destroy the target. | Open and usable, with associated == False. |
deallocate() |
A target this pointer was used to allocate. | Open and usable, with associated == False. |
close() |
This handle's descriptor storage. It does not destroy the target. | Permanently closed and unusable. |
Returned and caller-created descriptors close automatically when Python no
longer uses them. Call close() explicitly only when immediate descriptor
release matters. It never destroys the pointer target because the descriptor
and target have separate lifetimes.
Calling close() on a module or field pointer handle does nothing. It leaves
the descriptor, target, and handle unchanged.
A module handle observes the live module pointer descriptor. A field handle retains its parent wrapper and observes the live pointer component inside it. Native reassociation is visible through the same Python handle:
p = api.values
assert not p.associated
api.associate_values()
assert p.associated
api.choose_different_values()
print(p.shape) # reflects the new targetA pointer-array function result becomes a returned PointerArray. The handle
has persistent descriptor storage, but the target can belong to another
object:
p = api.selected_values(True)
if p.associated:
print(p.shape)An unassociated native result is a present handle with associated == False.
It is not returned as None.
A nonoptional pointer-array intent(out) does not consume an incoming
association, so it is hidden and returned as a new handle:
p = api.select_values()An optional intent(out) remains visible so omission can preserve native
present(...) behavior. Pointer-array intent(inout) also remains visible
because its incoming association is part of the call:
p = api.values
api.reassociate_values(p)
assert p.associated # the same descriptor was updated in placeFor an optional pointer descriptor, omission or None means the native
argument is absent. Passing an unassociated handle makes the argument present
but unassociated.
Create pointers.f90:
module pointers_api
implicit none
real(8), target :: storage(6) = [1, 2, 3, 4, 5, 6]
real(8), pointer :: values(:) => null()
contains
subroutine associate_values()
values => storage(1:6:2)
end subroutine associate_values
real(8) function sum_pointer(p) result(total)
real(8), pointer, intent(in) :: p(:)
if (associated(p)) then
total = sum(p)
else
total = -1.0_8
end if
end function sum_pointer
end module pointers_apiBuild and use it:
python3 -m prik pointers.f90 --out-dir build/pointersimport sys
sys.path.insert(0, "build/pointers")
import pointers.pointers_api as pointers_api
handle = pointers_api.values
assert not handle.associated
assert pointers_api.sum_pointer(handle) == -1.0
pointers_api.associate_values()
assert handle.associated
assert pointers_api.sum_pointer(handle) == 9.0
handle.nullify()
assert not handle.associatedA pointer may describe a whole array, a contiguous section, or a strided section:
real(8), target :: storage(6) = [10, 20, 30, 40, 50, 60]
real(8), pointer :: selected(:)
selected => storage(1:6:2)The NumPy view preserves that layout:
view = api.selected.to_numpy()
print(view) # [10. 30. 50.]
print(view.shape) # (3,)
print(view.strides) # (16,) for eight-byte elements
view[1] = 99.0 # updates storage(3)A strided pointer can be passed to a pointer-descriptor parameter. Passing the same handle to an ordinary array parameter that requires contiguous data is rejected.
Pointer safety depends on the target owner and lifetime, not only on descriptor state.
view = p.to_numpy()
view[0] = 1.0 # NOT OK: view may be Noneif p.associated:
view = p.to_numpy()
if view is not None:
view[0] = 1.0Association is necessary but cannot prove that an externally managed target is still alive. Native code must not leave a pointer associated with expired storage.
function invalid_result() result(values)
real(8), target :: local_values(3)
real(8), pointer :: values(:)
values => local_values ! NOT OK: local_values expires on return
end function invalid_resultPutting the pointer inside a returned derived object does not repair this native lifetime error.
view = p.to_numpy()
saved = None if view is None else view.copy()
api.point_at_different_storage()
current = p.to_numpy()Do not use view after target deallocation, reassociation, resizing, or
reallocation behind the pointer. Extract current for the new target. The
independent saved copy remains safe.
p.allocate(10)
p.deallocate()The allocate() may be called from Python or from a native routine using the
same pointer. If the pointer was only associated with existing storage, use
nullify() instead:
p.nullify() # does not destroy the targetDo not use nullify(), associate(), or close() while p is responsible
for an allocated target. The target remains allocated and may become
unreachable. Use p.deallocate() first.
Use resize() only in the same cases where deallocate() is valid.
first = api.first_pointer
second = api.second_pointer
assert first.associated and second.associated
first.nullify()
assert second.associatednullify() removes only first's association. It does not destroy the target
or change second. Deallocating their shared target makes every pointer to
that target invalid.
view = returned_pointer.to_numpy()
returned_pointer.close()
returned_pointer.shape # NOT OK: the descriptor has been releasedclose() releases a returned descriptor but never deallocates its target.
An existing NumPy view may still refer to the target, but its safety now depends
entirely on that target's separate owner and lifetime. Do not use the closed
handle to reason about the view.
strided = api.selected_slice
api.requires_contiguous_array(strided) # NOT OK: rejected before the callPass the descriptor to a pointer parameter, or make an explicit contiguous copy when ordinary array data is required:
view = strided.to_numpy()
copy = None if view is None else view.copy(order="F")view = p.to_numpy()
# Another thread reassociates or deallocates p here.
value = view[0] # NOT OK without native synchronizationprik does not lock native pointer association or track outstanding NumPy views. The application must synchronize concurrent native changes.
Scalar pointers appear as T | None values at the Python boundary rather than
PointerArray handles. An unassociated projected scalar result becomes None.
Scalar values do not expose persistent association, to_numpy(), or pointer
descriptor operations.
For an optional scalar pointer argument, omission makes the argument absent.
Passing None makes it present but unassociated, while passing a value makes it
present with that value. See Optional Arguments.
- Review Memory Management for the ownership and live view rules shared by all native storage.
- Compare Allocatables when the native object owns an allocation rather than a pointer association.