tur/forms/unsafe
the unsafe block, raw pointers, raw memory and dynamic loading.
The compiler implements these itself (src/compiler/elab_unsafe.c), so there is nothing to import. This file is reference only: it declares nothing the compiler reads.
Every builtin here is refused outside an enclosing (unsafe ...) block or a function declared #fx{Unsafe}. None is checked at run time: a bad pointer or offset is undefined behaviour, exactly as in C.
Pointers are ptr<void>, and the pointer builtins read and write whole 8-byte words (int64_t): ptr-deref is *(int64_t *)p, the unchecked array builtins index in words, and ptr-add / ptr-sub offset in bytes. A ptr<void> from raw-malloc is move-typed: passing it to a function consumes it (TUR-E0201 on a second use), while the builtins here borrow it.
unsafe
(unsafe body ...)
run code that uses the unsafe builtins.
Evaluates the body forms in order and returns the last one's value. Inside it, the builtins on this page and calls to #fx{Unsafe} functions are allowed. The block discharges the Unsafe effect: a function whose unsafe code is all inside (unsafe ...) has no Unsafe in its row, so a safe wrapper over an unsafe primitive is an ordinary function to its callers. A function annotated #fx{Unsafe} gets the same permission for its whole body, and its callers need a block of their own.
Lints: an empty block warns; a block of more than 20 forms warns (change the limit with --lint-unsafe-max-lines=N); --lint-unsafe also warns on a nested block.
| body | the forms to evaluate |
The value of the last body form.
(unsafe (+ 1 2)) ; => 3
raw-malloc
(raw-malloc [size :int] :ptr<void>)
allocate uninitialized memory.
C's malloc. Release the block with raw-free. The result is move-typed; see the module notes.
| size | the number of bytes |
A pointer to the block, or a null pointer when allocation fails.
(let [p (raw-malloc 8)] (ptr-write p 42) (let [v (ptr-deref p)] (raw-free p) v)) ; => 42
raw-free
(raw-free [p :ptr<void>] :nil)
release memory from raw-malloc or raw-realloc.
C's free. A null pointer is a no-op; freeing anything else twice is undefined behaviour.
| p | the block to release |
(let [p (raw-malloc 8)] (raw-free p))
raw-realloc
(raw-realloc [p :ptr<void> size :int] :ptr<void>)
resize a block, keeping its contents.
C's realloc: the block may move, so use the returned pointer and not p.
| p | the block to resize (or a null pointer, to allocate) | |
| size | the new size in bytes |
The resized block, or a null pointer when allocation fails (p is then
still valid).
(let [p (raw-realloc (raw-malloc 8) 64)] (array-set-unchecked p 7 3) (let [v (array-get-unchecked p 7)] (raw-free p) v)) ; => 3
raw-memcpy
(raw-memcpy [dest :ptr<void> src :ptr<void> n :int] :nil)
copy bytes between two blocks.
C's memcpy: the blocks must not overlap.
| dest | where to copy to | |
| src | where to copy from | |
| n | the number of bytes |
(let [p (raw-malloc 8) q (raw-malloc 8)] (ptr-write p 1234) (raw-memcpy q p 8) (let [v (ptr-deref q)] (raw-free p) (raw-free q) v)) ; => 1234
raw-memset
(raw-memset [dest :ptr<void> byte :int n :int] :nil)
fill bytes with one value.
C's memset: the low 8 bits of byte are written to each of the n bytes at dest.
| dest | the block to fill | |
| byte | the fill value | |
| n | the number of bytes |
(let [p (raw-malloc 16)] (raw-memset p 0 16) (let [v (array-get-unchecked p 1)] (raw-free p) v)) ; => 0
ptr-deref
(ptr-deref [p :ptr<void>] :int)
read the 8-byte word a pointer points at.
*(int64_t *)p. The word is returned as an int, whatever was stored.
| p | the address to read |
The word at p.
(let [x 41] (ptr-deref (ptr-of x))) ; => 41
ptr-write
(ptr-write [p :ptr<void> value :int] :nil)
store an 8-byte word through a pointer.
*(int64_t *)p = value.
| p | the address to write | |
| value | the word to store |
(let [p (raw-malloc 8)] (ptr-write p 7) (let [v (ptr-deref p)] (raw-free p) v)) ; => 7
ptr-add
(ptr-add [p :ptr<void> n :int] :ptr<void>)
advance a pointer by a number of BYTES.
(char *)p + n: the offset is in bytes, not words -- the second word of a block is (ptr-add p 8).
| p | the base address | |
| n | the offset in bytes |
The address n bytes past p.
(let [p (raw-malloc 16)] (ptr-write (ptr-add p 8) 7) (let [v (array-get-unchecked p 1)] (raw-free p) v)) ; => 7
ptr-sub
(ptr-sub [p :ptr<void> n :int] :ptr<void>)
move a pointer back by a number of BYTES.
(char *)p - n.
| p | the base address | |
| n | the offset in bytes |
The address n bytes before p.
(let [p (raw-malloc 16)] (ptr-write p 5) (let [v (ptr-deref (ptr-sub (ptr-add p 8) 8))] (raw-free p) v)) ; => 5
ptr-null?
(ptr-null? [p :ptr<void>] :bool)
is a pointer null?
| p | the pointer to test |
true for a null pointer.
(let [p (raw-malloc 8)] (let [n (ptr-null? p)] (raw-free p) n)) ; => false
ptr-of
(ptr-of [x :T] :ptr<void>)
the address of a value.
C's &. The pointer is good only while the value's storage is live -- the address of a local is dangling once its scope ends.
| x | the value whose address is wanted |
A pointer to x.
(let [x 41] (ptr-deref (ptr-of x))) ; => 41
array-get-unchecked
(array-get-unchecked [p :ptr<void> i :int] :int)
read word i of a block, with no bounds check.
((int64_t *)p)[i]: the index counts 8-byte words, not bytes.
| p | the block | |
| i | the word index |
The word at index i.
(let [p (raw-malloc 24)] (array-set-unchecked p 2 99) (let [v (array-get-unchecked p 2)] (raw-free p) v)) ; => 99
array-set-unchecked
(array-set-unchecked [p :ptr<void> i :int value :int] :nil)
write word i of a block, with no bounds check.
((int64_t *)p)[i] = value.
| p | the block | |
| i | the word index | |
| value | the word to store |
(let [p (raw-malloc 24)] (array-set-unchecked p 0 1) (let [v (ptr-deref p)] (raw-free p) v)) ; => 1
unsafe-cast
(unsafe-cast x type)
a C cast to int.
(int64_t)x. The target type is required but not read yet: the result is always an int, converted as C converts (a float truncates, true is 1).
| x | the value to convert | |
| type | the target type (currently ignored) |
x as an int.
(unsafe-cast true :int) ; => 1
reinterpret
(reinterpret x type)
reinterpret a value's bits as another type.
The same bit copy as transmute: the target type must have the same size as x's, checked at compile time, and the result has the target type. Nothing is converted -- (reinterpret 1.5 :int) is 1.5's IEEE-754 bit pattern, not 1.
| x | the value | |
| type | the target type, a primitive kind such as :int |
x's bits, read as the target type.
(reinterpret 1.5 :int) ; => 4609434218613702656
transmute
(transmute x type)
reinterpret a value's bits as another type.
The target type must have the same size as x's, checked at compile time ((transmute (:: 1 :int32) :int) is an error), and the result has the target type. The bits are copied, not converted: (transmute 1.5 :int) is 4609434218613702656, and transmute back to :float gives 1.5. float->bits / bits->float (tur/bits) are the same copy outside an unsafe block. For the NUMBER, use unsafe-cast or float->int.
| x | the value | |
| type | the target type, a primitive kind such as :float |
x's bits, read as the target type.
(transmute 4609434218613702656 :float) ; => 1.5
c-call
(c-call f args ...)
call a C function declared with extern-c.
The function must be in scope from an extern-c declaration. The result is typed int, whatever the declaration says; calling the extern-c name directly keeps its declared types and needs no (unsafe ...).
| f | the extern-c function's name | |
| args | its arguments |
The C function's result, as an int.
(extern-c labs [x : int] : int) (unsafe (c-call labs -5)) ; prints 5
dlopen
(dlopen [path :cstr] :ptr<void>)
load a shared library at run time.
POSIX dlopen(path, RTLD_LAZY). Look symbols up with dlsym and call them through call-ptr; see ffi-guide. Needs the FFI capability under a sandboxed interpreter.
| path | the library's file name or path |
A library handle, or a null pointer when it cannot be loaded.
(let [h (dlopen "libm.so.6")] (let [ok (not (ptr-null? h))] (dlclose h) ok)) ; => true ; doctest: needs libm.so.6 (Linux)
dlsym
(dlsym [h :ptr<void> name :cstr] :ptr<void>)
look a symbol up in a loaded library.
POSIX dlsym.
| h | the handle from dlopen | |
| name | the symbol's name |
The symbol's address, or a null pointer when the library lacks it.
(let [h (dlopen "libm.so.6")] (let [ok (not (ptr-null? (dlsym h "cos")))] (dlclose h) ok)) ; => true ; doctest: needs libm.so.6 (Linux)
dlclose
(dlclose [h :ptr<void>] :int)
release a library handle from dlopen.
POSIX dlclose. Addresses from dlsym on it may be invalid afterwards.
| h | the handle to release |
0 on success, non-zero on failure.
(dlclose (dlopen "libm.so.6")) ; => 0 ; doctest: needs libm.so.6 (Linux)