Turmeric's type checker cannot see inside a raw pointer. The handful of
builtins that read and write memory directly -- ptr-deref, raw-malloc,
transmute and the rest -- are therefore allowed only where a program says it
is taking responsibility for them: inside an (unsafe ...) block, or in a
function declared #fx{Unsafe}. This guide says what each one does, what it
does not check, and what to use instead when you do not actually need it.
The per-form reference is tur/forms/unsafe; this page is the when-and-why.
unsafe block and the Unsafe effect(defn first-word [p : ptr<void>] : int
(unsafe (ptr-deref p)))
Outside the block, (ptr-deref p) is refused at compile time. Inside it, the
builtins on this page and calls to #fx{Unsafe} functions are allowed. The
block discharges the Unsafe effect: first-word above has no Unsafe in
its row, so its callers treat it as an ordinary function. That is the point of
the block -- a safe wrapper over an unsafe primitive, with the unsafety kept in
one place.
A function declared #fx{Unsafe} gets the same permission for its whole body,
and passes the obligation on: each caller needs a block (or #fx{Unsafe}) of
its own.
(defn poke! [p : ptr<void> v : int] #fx{Unsafe} : nil
(ptr-write p v))
Lints keep the blocks small: an empty block warns, a block of more than 20
forms warns (--lint-unsafe-max-lines=N changes the limit), and
--lint-unsafe also warns on a block nested in another. Borrowing from a
ptr<void> ((& p)) needs the block too.
Under a sandboxed interpreter these builtins need the TURI_CAP_UNSAFE
capability (sandboxing-guide).
| Form | C | Notes |
|---|---|---|
(raw-malloc n) |
malloc(n) |
n bytes, uninitialised; a null pointer on failure |
(raw-free p) |
free(p) |
a null pointer is a no-op |
(raw-realloc p n) |
realloc(p, n) |
the block may move: use the result, not p |
(raw-memcpy dst src n) |
memcpy |
the blocks must not overlap |
(raw-memset dst b n) |
memset |
the low 8 bits of b, into each of n bytes |
The pointer from raw-malloc is move-typed: handing it to a function
consumes it, and a second use is TUR-E0201. The builtins on this page only
borrow it, so a block can read, write and free the same pointer.
The pointer builtins treat memory as 8-byte words (int64_t), and that is the
one thing to remember about them:
| Form | C | Unit |
|---|---|---|
(ptr-deref p) |
*(int64_t *)p |
reads one word, returned as int |
(ptr-write p v) |
*(int64_t *)p = v |
writes one word |
(array-get-unchecked p i) |
((int64_t *)p)[i] |
i counts words |
(array-set-unchecked p i v) |
((int64_t *)p)[i] = v |
i counts words |
(ptr-add p n) / (ptr-sub p n) |
(char *)p + n / - n |
n counts bytes |
(ptr-null? p) |
p == NULL |
|
(ptr-of x) |
&x |
dangling once x's scope ends |
So the second word of a block is (array-get-unchecked p 1) or
(ptr-deref (ptr-add p 8)) -- not (ptr-add p 1).
(defn squares-sum [n : int] : int
(unsafe
(let [p (raw-malloc (* n 8))
^mut i 0]
(while (< i n)
(array-set-unchecked p i (* i i))
(set! i (+ i 1)))
(let [last (ptr-deref (ptr-add p (* (- n 1) 8)))
^mut acc 0
^mut j 0]
(while (< j n)
(set! acc (+ acc (array-get-unchecked p j)))
(set! j (+ j 1)))
(raw-free p)
(+ acc last)))))
(defn main [] : int
(println (squares-sum 4)) ; 0+1+4+9 = 14, plus the last word 9 => 23
0)
Nothing here is checked at run time. An index past the block, a pointer used
after raw-free, or a ptr-of that outlived its local is undefined behaviour,
exactly as in C. A Debug build of a program compiled with
TUR_CC_FLAGS=-fsanitize=address is the quickest way to find one.
| Form | What it does |
|---|---|
(unsafe-cast x T) |
(int64_t)x: a C conversion to int. T is required but not read; a float truncates, true is 1 |
(transmute x T) |
copies x's bits into a T of the same size (checked at compile time) |
(reinterpret x T) |
the same bit copy as transmute |
(defn main [] : int
(unsafe
(println (transmute 1.5 :int)) ; 4609434218613702656
(println (transmute (transmute 1.5 :int) :float)) ; 1.5
(println (unsafe-cast 7.9 :int))) ; 7
0)
(transmute (:: 1 :int32) :int) is a compile error: the sizes differ, and
there is no defined way to fill the missing bytes.
(c-call f args ...) calls a function declared with extern-c, with the
result typed int whatever the declaration says. Calling the extern-c name
directly keeps the declared types and needs no block, so prefer that; c-call
is for the rare call whose declared result you want erased. dlopen, dlsym
and dlclose load libraries at run time; ffi-guide covers them
with call-ptr.
Most programs that want one of these want something else:
| You want | Use instead |
|---|---|
| a float's bit pattern, or a float from bits | float->bits / bits->float (tur/bits), no block needed |
| the number in a float, as an int | float->int (tur/math) or (as int x) |
| to call a C library | extern-c, which keeps the declared types (c-integration-guide) |
| a buffer of words | (Vec int) -- bounds-checked, freed with vec-free |
| a fallible C constructor | inline C returning (Result T E) (inline-c-results-guide) |
| an opaque C handle | defopaque, so the checker can tell handles apart |
dlopen / dlsym / call-ptrextern-c and inline CUnsafe effect is checkedTURI_CAP_UNSAFE