| title | Strings |
|---|---|
| description | Immutable strings, mutable character storage, and NumPy byte arrays in prik |
| audience | users |
| prerequisites | data types, arrays |
| related | data-types.md, arrays.md, raw-addresses.md |
| status | maintained |
| publication | reviewed |
prik uses Python str for scalar character values.
Mutable character storage uses fixed-width NumPy bytes arrays.
The contract decides whether native mutation becomes a new str or changes
caller-owned storage.
| Contract | Python value | Native mutation |
|---|---|---|
String |
Variable-length str |
Returned only when projected |
String[8] |
str encoded as exactly 8 bytes |
Returned as a new str |
String[8][()] |
Rank-zero NumPy array with dtype S8 |
Visible in place |
String[8][count] |
NumPy bytes array with dtype S8 |
Visible in place |
Addr(String[8]) |
Integer address | Visible through caller-owned memory |
Use normal string and NumPy contracts by default. Raw addresses are an
advanced boundary covered later in the guide.
Returns[...] tells the wrapper to return the changed value of an argument.
The source, generated contract, edited contract, and Python calls below describe the same string boundaries. The results remain visible below the four views.
Create strings_api.f90:
module strings_api
implicit none
contains
subroutine edit_text(text)
character(len=8), intent(inout) :: text
text(1:1) = "X"
end subroutine edit_text
subroutine edit_buffer(text)
character(len=8), intent(inout) :: text
text(1:1) = "X"
end subroutine edit_buffer
function make_text() result(text)
character(len=8) :: text
text = "ready"
end function make_text
subroutine edit_labels(count, labels)
integer(4), intent(in) :: count
character(len=8), intent(inout) :: labels(count)
integer(4) :: index
do index = 1, count
labels(index)(1:1) = "X"
end do
end subroutine edit_labels
end module strings_apiThe generated contracts/strings/strings_api.pyi is:
from prik.contracts import Addr, Arg, Int32, Returns, String, native_call
def edit_text(
text: String[8]
) -> Returns["text", String[8]]: ...
def edit_buffer(
text: String[8]
) -> Returns["text", String[8]]: ...
def make_text() -> String[8]: ...
@native_call([Addr(Arg(0)), Arg(1)])
def edit_labels(
count: Int32,
labels: String[8][count]
) -> None: ...Generate it:
python3 -m prik generate --pyi strings_api.f90 --out contracts/stringsOnly edit_buffer changes. Rank-zero storage makes native mutation visible in
place rather than returning a replacement value. The edited
contracts/strings/strings_api.pyi is:
from prik.contracts import Addr, Arg, Int32, Returns, String, native_call
def edit_text(
text: String[8]
) -> Returns["text", String[8]]: ...
def edit_buffer(text: String[8][()]) -> None: ...
def make_text() -> String[8]: ...
@native_call([Addr(Arg(0)), Arg(1)])
def edit_labels(
count: Int32,
labels: String[8][count],
) -> None: ...Build from the edited contract and native source:
python3 -m prik contracts/strings/__init__.pyi \
--native-fortran-sources strings_api.f90 \
--out-dir build/stringsFor the complete result-mapping rules, see Reorder Arguments and Project Outputs.
String[8] accepts a Python str whose encoded length is exactly eight bytes.
The wrapper copies it into native storage.
import sys
sys.path.insert(0, "build/strings")
from strings.strings_api import edit_text, make_text
original = "alpha "
changed = edit_text(original)
print(repr(original)) # 'alpha '
print(repr(changed)) # 'Xlpha '
print(repr(make_text())) # 'ready 'Python strings are immutable. Returns[...] copies the changed native buffer
into a new str. Without that projection, the mutation is discarded.
String[8][()] accepts a rank-zero NumPy bytes array.
Native writes change the same object.
import sys
import numpy as np
sys.path.insert(0, "build/strings")
from strings.strings_api import edit_buffer
buffer = np.array("alpha ", dtype="S8")
edit_buffer(buffer)
print(buffer[()]) # b'Xlpha 'The public value is bytes storage. Reading buffer[()] returns np.bytes_,
not str.
String arrays use fixed-width NumPy bytes dtypes. The dtype item size is the Fortran character length.
import sys
import numpy as np
sys.path.insert(0, "build/strings")
from strings.strings_api import edit_labels
labels = np.array([b"alpha ", b"beta "], dtype="S8")
edit_labels(np.int32(labels.size), labels)
print(labels) # [b'Xlpha ' b'Xeta ']The wrapper checks rank, shape, dtype, and writeability before the call. Unicode and object arrays are rejected.
Result:
'alpha '
'Xlpha '
'ready '
b'Xlpha '
[b'Xlpha ' b'Xeta ']
String[8]requires exactly eight encoded bytes.Stringaccepts a runtime character length.- Fixed-width results retain trailing Fortran blanks.
- Embedded NUL bytes are rejected for scalar Python strings.
String[8][()]andString[8][count]require dtypeS8.- A dummy without
intentuses the conservativeintent(inout)behavior.
Mutable deferred-length scalar storage is not supported. Use a fixed-width buffer or an immutable replacement result.
- Continue with Wrapping Functions.
- Wrapping Subroutines for complete
intentand result-projection rules. - Raw Addresses for the advanced
Addr(String[n])boundary.