Unsafe code and interoperability
Unsafe and interop features are for working with native APIs, pointers, and storage that cannot be expressed with ordinary managed values. Keeping them in an explicit unsafe context makes that boundary visible.
The *Type form declares a native pointer to Type. Pointer types are
distinct from by-reference types but interoperate with address-of
expressions: taking the address of a local or field produces an address
handle that implicitly converts to the matching pointer type.
Pointer declarations and pointer-type usage require unsafe mode. Enable
unsafe mode with the compiler's --unsafe option; otherwise pointer type
syntax is rejected.
Unsafe context can be enabled either globally (compiler option --unsafe)
or locally with unsafe contexts:
unsafe func Name(...) { ... }for function/method scopeunsafe { ... }as a statement for block scopeunsafe { ... }as an expression for a value-producing block scope
An unsafe block expression has the same value and type as its inner block expression. It only enables unsafe operations while binding that block; code before and after the expression remains in the surrounding unsafe context.
val value = 42
val pointer: *int = &value
Pointer operations
Pointer operations read, write, or move through native memory directly.
Pointer operations are also gated by unsafe mode. In unsafe mode:
*ptrdereferences a pointer (or by-reference value) and reads the pointed value.*ptr = valuewrites through the pointer.ptr->Memberaccesses a member on the pointed-at type and is equivalent to(*ptr).Member, while preserving direct storage semantics (no implicit struct copy for instance member calls/field writes).ptr + n/n + ptradvances a pointer bynelements.ptr - nrewinds a pointer bynelements.ptr1 - ptr2returns the element-distance asnintwhen both pointers share the same element type. Arithmetic offsets are scaled bysizeof(element-type).
var value = 41
let pointer: *int = &value
*pointer = 42
val result = *pointer // 42
struct Holder {
field Foo: int = 0
}
unsafe func assignThroughPointer() -> int {
var holder = Holder()
let pointer: *Holder = &holder
pointer->Foo = 2
holder.Foo // 2
}
Stack allocation
stackalloc T[count] reserves contiguous storage for count values of T in
the current stack frame. Without another target it produces System.Span<T>;
an explicit System.ReadOnlySpan<T> target produces a read-only view. An
explicit *T target selects native-pointer behavior.
func sumFour() -> int {
val values = stackalloc int[4]
values[0] = 10
values[1] = 20
values[2] = 30
values[3] = 40
values[0] + values[1] + values[2] + values[3]
}
unsafe func pointerForm() -> int {
val values: *int = stackalloc int[1]
*values = 42
*values
}
Rules:
- The element type must be unmanaged: it cannot contain managed references.
- The count must be implicitly convertible to
intand may be computed at runtime. - A negative constant count is rejected during compilation.
Span<T>andReadOnlySpan<T>targets are available in safe code.- An explicit pointer target requires an unsafe context.
- The resulting storage is valid only while the declaring stack frame remains
active and must not be returned or otherwise allowed to escape. This applies
both to the allocation result itself and to pointer,
Span<T>, orReadOnlySpan<T>locals and aliases backed by that allocation. Other span values, such as parameters and spans backed by managed arrays, may be returned.
Pinning managed storage
Pinning keeps managed storage at a stable address while native code or a pointer uses it.
Raven uses a use-scoped pinning form instead of a dedicated fixed (...) { ... }
statement. The initializer syntax is:
unsafe {
use pointer: *int = fixed &value
}
Rules:
fixedis only valid as the initializer of ausedeclaration.fixedrequires an explicit address-of operand written as&expr.- The result type is a native pointer
*T, whereTis the addressed storage type. - The pin lasts for the lifetime of the enclosing
usescope and is released when that scope exits, including exceptional exits. fixedis gated by unsafe mode just like other pointer-producing operations.
The explicit & keeps address selection separate from pinning: &expr identifies the storage, while fixed guarantees that storage remains stable for pointer use within the use scope.
unsafe func writeSecond(values: int[]) -> int {
use pointer = fixed &values[1]
*pointer = 42
values[1]
}
Extern declarations
Use an extern declaration for a function whose implementation is supplied
outside Raven, such as a native library entry point.
Methods and function statements can be marked extern to declare that their
implementation is provided externally (for example via P/Invoke). Extern
declarations do not include a body.
Rules:
- Type members marked
externmust also bestatic. externmembers/functions cannot declare a block body or expression body.- P/Invoke declarations are expressed with
[DllImport(...)]on static extern methods.
import System.Runtime.InteropServices.*
class Native {
[DllImport("kernel32", EntryPoint: "GetTickCount")]
extern static GetTickCount() -> uint;
}
Pass by reference
Passing or returning a value by reference lets code work with the caller's existing storage instead of a copy.
By-reference types can annotate locals and return values. A local
declared with &Type acts as an alias to the underlying storage, so
assignments flow through to the referenced location. Functions may
return by-reference values to expose existing storage to the caller. If
you plan to reassign the alias, declare it with var so the reference
itself remains mutable.
Taking the address of a value with &expression implicitly produces a
by-reference type when the target binding has no explicit annotation.
Use a pointer annotation to force the result into a native pointer
instead of a managed alias.
func headSlot(values: int[]) -> &int {
return &values[0]
}
var numbers: int[] = [10, 20, 30]
var slot = headSlot(numbers)
slot = 42 // numbers[0] is now 42
val value = 0
val alias = &value // alias : &int
val raw: *int = &value // raw : *int
By-reference returns must point to storage that outlives the callee. Returning
&local or &valueParameter is rejected because those addresses become invalid
after the function returns.
ref/out arguments
Parameters can also be declared with an explicit by-reference type using
&Type. That form is valid when the type itself is intentionally part of
the signature. Separately, ref, out, and in declare by-reference
parameter passing at the call boundary and already imply aliasing, so
their parameter types are written as plain Type, not &Type. For
example, write ref value: int, not ref value: &int. When a
by-reference parameter is passed into a function, it behaves just
like a by-reference local: the callee receives an alias to the caller's
storage and can both read and write through that reference. To mark a
parameter that must be assigned by the callee before returning, place
out before the parameter name. Parameters are immutable by default, so
add the var modifier when you need to reassign the alias—for example
to satisfy an out contract or to reuse a ref parameter as scratch
storage. At call sites, pass the argument with the address operator
&. (Exact rules are contextual; the binder enforces that the target is
assignable.)
By-reference locals and fields never use the out modifier—out is
only meaningful at the call boundary to signal definite assignment
responsibilities between caller and callee. Declaring a local with
ref, out, and in parameters immediately alias existing storage
locations. The caller provides that storage with &expr; out
requires the callee to assign the aliased storage before returning.
func TryParse(text: string, out result: int) -> bool { /* ... */ }
var total = 0
if !TryParse(arg, out total) {
Console.WriteLine("Expected number")
}