Pointers¶
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.
Key Concepts¶
- 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.
When To Use A Pointer Handle¶
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 Array Handle API¶
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 True
The 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.
Associate Two Pointers¶
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.
Nullify, Deallocate, And Close¶
| 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.
Where Handles Come From¶
Module Variables And Derived Fields¶
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 target
Function Results¶
A 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.
Output And Inout Arguments¶
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 place
For an optional pointer descriptor, omission or None means the native
argument is absent. Passing an unassociated handle makes the argument present
but unassociated.
Complete Module Example¶
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_api
Build and use it:
python3 -m prik pointers.f90 --out-dir build/pointers
import 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.associated
Contiguous And Strided Targets¶
A 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.
Safety Checklist¶
Pointer safety depends on the target owner and lifetime, not only on descriptor state.
Check Association And Lifetime¶
view = p.to_numpy()
view[0] = 1.0 # NOT OK: view may be None
if p.associated:
view = p.to_numpy()
if view is not None:
view[0] = 1.0
Association is necessary but cannot prove that an externally managed target is still alive. Native code must not leave a pointer associated with expired storage.
Do Not Return A Pointer To Expired Local 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_result
Putting the pointer inside a returned derived object does not repair this native lifetime error.
Copy Or Discard Views Before Target Changes¶
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.
Deallocate Only What This Pointer Allocated¶
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 target
Do 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.
Nullifying One Pointer Does Not Change Other Pointers¶
first = api.first_pointer
second = api.second_pointer
assert first.associated and second.associated
first.nullify()
assert second.associated
nullify() 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.
Do Not Keep Using A Closed Handle¶
view = returned_pointer.to_numpy()
returned_pointer.close()
returned_pointer.shape # NOT OK: the descriptor has been released
close() 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.
Respect Contiguity Requirements¶
strided = api.selected_slice
api.requires_contiguous_array(strided) # NOT OK: rejected before the call
Pass 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")
Synchronize Target Changes¶
view = p.to_numpy()
# Another thread reassociates or deallocates p here.
value = view[0] # NOT OK without native synchronization
prik does not lock native pointer association or track outstanding NumPy views. The application must synchronize concurrent native changes.
Scalar Pointers¶
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.
Next¶
- 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.