Calls and Results

The function signature describes the Python call. @native_call(...) describes how that call supplies the native arguments.

Expose Native Arguments Directly

When every native argument is visible in native order, @native_call(...) is not needed:

from prik.contracts import Int32

def scalar_status(
    base: Int32[()],
    status: Int32[()],
) -> None: ...

Writable scalar slots use zero-dimensional NumPy arrays:

import numpy as np

base = np.array(4, dtype=np.int32)
status = np.empty((), dtype=np.int32)

module.scalar_status(base, status)
print(status[()])

This form also works for arrays and derived objects when their annotations match the native arguments. For a fixed-width string, a Python str can be passed, but changes made to its temporary native buffer are not visible unless the contract returns a replacement.

Reorder Arguments and Project Outputs

Use Returns[...] for a Python result and @native_call(...) when the native procedure needs hidden output storage, reordered arguments, constants, lengths, presence flags, shapes, or work buffers:

from prik.contracts import Addr, Arg, Int32, Return, Returns, native_call

@native_call([Addr(Arg(0)), Return("status", 0)])
def scalar_status(base: Int32) -> Returns["status", Int32]: ...

Here Python passes one value and receives the native status output. Every required native argument must appear exactly once in the mapping. Missing, duplicate, and out-of-range positions are errors.

The mapping may use entries such as Arg(...), Addr(...), Value(...), Len(...), IsPresent(...), and Work(...). These entries describe the existing native call; they cannot change what the implementation accepts. The complete projection grammar is covered by the .pyi Format reference.

Preserve an Exact C Scalar at the Native Call

A target-specific C contract may intentionally expose two distinct C types as the same NumPy dtype. For example, both long and long long may use signed 64-bit values, so both public signatures use Int64. C still treats the two native types as distinct.

Use an exact C scalar identity only around the affected native-call expression:

from prik.contracts import Arg, CLongLong, Float64, Int64, native_call

@native_call([CLongLong(Arg(0)), Arg(1)])
def accumulate(count: Int64, scale: Float64) -> None: ...

The public annotation is authoritative: the user passes a NumPy int64 and never has to spell numpy.longlong merely because CLongLong appears in @native_call. The binding extracts the public value into int64_t, then emits the native call as:

accumulate((long long)contract_count, contract_scale);

A scalar crosses by value, so the binding converts it: either NumPy spelling of that width is accepted. np.int64 and np.longlong both satisfy a long long parameter, and both satisfy a long parameter, whichever one the target calls int64_t.

An array is different. Its elements are handed to C as the buffer they already are, and a buffer cannot be converted element by element, so an array argument whose @native_call entry names an exact C identity accepts only that element dtype:

@native_call([CLongLong(Arg(0)), Arg(1)])
def increment(values: Int64[:], count: Int32) -> None: ...
increment(np.array([1, 2, 3], dtype=np.longlong), np.int32(3))   # accepted
increment(np.array([1, 2, 3], dtype=np.int64), np.int32(3))      # TypeError

A generated array contract follows the same rule from the C source alone: the accepted dtype is that of the declared C element type, so a long long * buffer wants numpy.longlong and a long * buffer wants numpy.int64 on every target, whichever one that target calls int64_t. The generated docstring names the exact dtype, so check it when a long/long long distinction is in play.

The same sparse form records a native function result whose C identity was lost by width-based normalization:

from prik.contracts import Arg, CLongLong, Float64, Int64, Return, native_call

@native_call([Arg(0)], result=CLongLong(Return(0)))
def llround(value: Float64) -> Int64: ...

The decorator position determines the direction. A native scalar wrapper in the ordered list describes a native parameter; it may wrap Arg(i) or an output-parameter Return(i). In result=..., it declares the native function result. Here the binding declares a long long result, receives it, and converts it into the public Int64 result slot selected by Return(0).

Unchanged arguments and results retain their ordinary lowering. Native C scalar names are call-expression operators: using CLongLong or CLong as a function annotation, field type, or return annotation is an error. Generated C contracts add these operators only when the active target's canonical contract storage is not C-compatible with the source declaration.

For a scalar address, conversion happens before taking the address:

from prik.contracts import Addr, Arg, CLongLong, Int64, Returns, native_call

@native_call([Addr(CLongLong(Arg(0)))])
def update(value: Int64) -> Returns["value", Int64]: ...

This converts the extracted int64_t into a long long call-local and passes that local's address, so the callee receives a genuine long long *. It never casts int64_t * to an incompatible pointer type. Naming the argument in Returns[...] projects the updated call-local back into the public Int64 dtype; Python scalar inputs are immutable, so an updated scalar always reaches Python as a result rather than in place. A bare -> Int64 declares something different — that the native function itself returns an int64_t — and leaves the update invisible.

For a ranked argument, the same operator selects the exact NumPy storage that can cross the pointer boundary without a cast:

@native_call([CLongLong(Arg(0))])
def update_many(values: Int64[:]) -> None: ...

The public value type remains signed 64-bit integer, but the caller must supply an array created with dtype=numpy.longlong when long long is distinct from the target's canonical int64_t. An ordinary numpy.int64 array is rejected on that target even when it has the same width and representation. The binding passes the accepted numpy.longlong storage directly as long long *; it does not reinterpret an incompatible pointer or allocate a conversion copy. This exact-storage rule applies to every supported signed, unsigned, real, and complex C scalar type with corresponding NumPy storage, including CLong, CUnsignedLongLong, and CLongDoubleComplex. C _Bool arrays remain unsupported because NumPy Boolean array storage is not C _Bool storage.

There is no intent annotation in the .pyi. The signature, Returns[...], and @native_call(...) are the complete contract after the file is loaded.

Control Mutation

Immutable means the original Python value must not change. A writable native argument then needs an explicit replacement result or a supported rule that discards the temporary mutation:

from prik.contracts import Annotated, Float64, Immutable, Returns

def scale(
    values: Annotated[Float64[:], Immutable],
) -> Returns["values", Float64[:]]: ...

PRIK calls the native procedure with separate writable storage and returns the replacement. The original array remains unchanged.

Do not combine replacement-only mutation with a writable borrowed view. Those requests contradict each other and are rejected.

Edit Types, Shapes, Layout, and Optionality

Annotations affect runtime checks; they are not only IDE hints:

from prik.contracts import Float64

def solve(
    matrix: Float64[3, 3],
    rhs: Float64[3],
) -> Float64[3]: ...

Supported edits include:

  • changing an open dimension to a fixed size;
  • selecting a supported contiguous layout;
  • adding Immutable for a supported replacement path; and
  • using T | None or a default = ... for an argument that is genuinely optional in the native procedure.

Changing dtype or rank, or inventing optionality, changes the declared native binary interface. It is valid only when the implementation matches. PRIK can check exact NumPy dtype, rank, shape, layout, writeability, byte order, alignment, and zero-sized-array rules. Plain multidimensional arrays in a Fortran contract use Fortran order by default. The array guide explains these checks.

Advanced Array Shape Expressions

Generated contracts may translate native array extents into Python and NumPy expressions:

Native relationship Semantic .pyi contract
total element count values.size
second-axis extent values.shape[1]
runtime rank values.ndim
extent from an integer argument rows or rows + 1
extent calculated by native code extent_for(n)

These expressions describe the existing native interface; they do not change what the implementation accepts. Most users should leave generated shape relationships unchanged. When editing one, use visible integer arguments and the documented array properties shown above.

A native extent function remains a declarative native call—it is not executed as Python. PRIK rejects unresolved, incompatible, or unsafe relationships instead of guessing an array size.

Translate Status Results into Exceptions

Use @raises(...) when a projected native status should become a Python exception:

from prik.contracts import Addr, Arg, Hidden, Int32, String, native_call, raises

@raises(status="status", message="message", success=0)
@native_call([Addr(Arg(0)), Hidden("status", Int32), Hidden("message", String[32])])
def solve(value: Int32) -> None: ...

Declare the status and any native-only message with Hidden(name, T): it is produced by the native call but never reaches Python, so it does not appear in the return annotation. A message may instead name a visible rank-zero NumPy bytes buffer that the caller supplies. A non-success status raises the generated exception before an ordinary result is returned. See Error Handling for the Python behavior.

Release the GIL for a Native Call

Native calls keep Python's Global Interpreter Lock (GIL) by default. Use @nogil only when the native call can safely run while other Python threads execute:

from prik.contracts import nogil

@nogil
def run_parallel_engine() -> None: ...

@nogil accepts no arguments and releases the GIL only around the native bridge call. Argument conversion, result conversion, writeback, cleanup, and exception projection still run with the GIL held. Remove @nogil to restore the default held-GIL behavior. This changes call behavior, not the native procedure interface.

If a decorated native call invokes a PRIK callback, the callback trampoline temporarily reacquires the GIL for Python execution. Callback contracts are covered in the Callbacks guide.

Next

Most users can stop here. Return to Editing .pyi Contracts to choose another edit.