Pointers qualified as handle
will automatically track the lifetime of an allocated type
instance by performing reference counting so when the last reference to a type
's instance is discarded, the type instance deallocated.
The handle
pointers are a form of smart pointer logic used as a tool to ensure the lifetime of type's instances are destroyed when the last referencing pointer is discarded. A hint
reference can be used to prevent circular dependencies by detecting when a type
's instance is kept alive by a handle
pointer or when a type
instance was already destroyed. The usage of hint
references is a common technique to prevent memory leakage issue as handle
pointers are not automatically garbage collected.
Thread safety is not addressed with handle
or hint
pointers and handle
pointers should not be used across thread boundaries unless every time a handle
pointer is fully transferred to another handle
pointer or the handle
is transferred to an own
pointer across a thread barrier. Alternatively a deep
copy can be performed on a handle
to ensure contents are fully copied.
Pointers qualified as handle
operate in the same manner as strong
pointers with a few key differences. Whereas strong
pointers have a weak
counterpart, handle
pointers have a hint
counterpart. Pointers qualified as strong
have thread safety properties related to a lifetime of an instance whereas handle
pointers do not.
Due to the overhead of thread safety guarantees for strong
pointers, handle
pointers are more efficient at the cost of thread safety.
Pointers qualified as handle
are allocated in similar manners to other pointers, such as own
, discard
, strong
, discard
, and collect
pointers. The difference is that handle
pointers can be co-owned by more than one variable. When the last variable holding the handle
pointer is discarded (or reset to empty) the allocated type is destructed and deallocated (unless a hint
pointer still exists to a combined control and allocation block for a type
's instance).
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
myValue1 : Integer
myValue2 : String
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the
// context's allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// `value2` is is a `handle` pointer but the value is initialized to
// point to nothing
value2 : MyType * handle
// both `value2` and `value1` have a handle pointer to the same
// `MyType` instance
value2 = value1
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
assert(value1 == value2)
// `value2` and `value1` pointers both fall out of scope but only
// the single `MyType` instance is destructed and deallocated
}
return "I'll be back."
}
Pointers qualified as handle
can only point to a single instance of a type
. If a pointer is reset to point to a new instance of a type
then the original ownership claim is released. If a variable was the last owner of a type
's instance then the type
's instance is discarded and the memory is deallocated.
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
myValue1 : Integer
myValue2 : String
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the
// context's allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// the @ operator allocates `value2` dynamically with the
// context's allocator and a `handle` pointer is maintained to
// another `MyType` instance
value2 : MyType * handle @
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
// `value1` and `value2` point to different `MyType` instances
assert(value1 != value2)
// `value2`'s value is replaced, thus `MyType` instance in `value2`
// is destructed and deallocated and then both `value1` and `value2`
// point to the same `MyType` instance
value2 = value1
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
// `value1` and `value2` point to the same `MyType` instances
assert(value1 == value2)
// `value2` and `value1` pointers both fall out of scope but only
// the single `MyType` instance originally allocated in `value1` is
// destructed and deallocated
}
return "I'll be back."
}
The lifetime of handle
pointers is entirely dependent on all handle
pointers pointing to the same instance of a type being discarded (or reset to point to empty). Only when the final handle
pointer is discarded or reset will be shared instance of a type
be released.
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
myValue1 : Integer
myValue2 : String
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (
// `stayingAlive` is a `handle` pointer defaulted to point to nothing
stayingAlive : MyType * handle
)() = {
scope {
// the @ operator allocates `value1` dynamically with the
// context's allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// the @ operator allocates `value2` dynamically with the
// context's allocator and a `handle` pointer is maintained to
// another `MyType` instance
value2 : MyType * handle @
// `stayingAlive` will point to `value2` thus they will both point to
// the same `MyType` instance
stayingAlive = value2
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(stayingAlive) // will print "true"
// `value1` and `value2` point to different `MyType` instances
assert(value1 != value2)
// `value2` and `stayingAlive` point to the same `MyType` instance
assert(stayingAlive == value2)
// `value2`'s value is replaced, thus `MyType` instance in `value2`
// is replaced but the original type's instance in `value2` is not
// destructed or deallocated as `stayingAlive` will keep the
// original instance allocated in `value2` alive.
value2 = value1
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(stayingAlive) // will print "true"
// `value1` and `value2` point to the same `MyType` instances
assert(value1 == value2)
// `stayingAlive` and `value2` point to different `MyType` instances
assert(stayingAlive != value2)
// `value2` and `value1` pointers both fall out of scope but only
// the single `MyType` instance originally allocated in `value1` is
// destructed and deallocated
}
// `stayingAlive` is returned to the caller and thus will not fall out of
// scope and the type originally allocated in `value2` is kept alive beyond
// the lifetime of this function call
return
}
func2 final : ()() = {
// the `handle` pointer returned by `func` is now being kept alive by
// `liveAndLetDie`
liveAndLetDie := func()
printIfValidPointer(liveAndLetDie) // will print "true"
// reset `liveAndLetDie` which causes the lifetime originally allocated in
// `value2` of `func` function to be destructed and deallocated
liveAndLetDie = #:
printIfValidPointer(liveAndLetDie) // will print "false"
}
Care must be used when dealing with handle
pointer lifetimes to ensure a circular dependency is not created where a circular chain is never broken. If a handle
pointer points to another handle
pointer that directly or indirectly points back to the original type's instance then the lifetime of the handle
pointers in the chain will never become destructed nor deallocated.
The example below creates a circular handle
pointer chain:
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
head : MyType * handle
next : MyType * handle
myValue1 : Integer
myValue2 : String
+++ final : ()() = {
print("This will be displayed three times in this example.")
}
--- final : ()() = {
print("BAD NEWS! This will never be displayed in this example!!!")
}
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the context's
// allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// the @ operator allocates `value2` dynamically with the context's
// allocator and a `handle` pointer is maintained to another `MyType`
// instance
value2 : MyType * handle @
// the @ operator allocates `value3` dynamically with the
// context's allocator and a `handle` pointer is maintained to
// another `MyType` instance
value3 : MyType * handle @
// setup pointers to the head of the linked list
value1.head = value1
value2.head = value1
value3.head = value1
// setup points to the linked list chain
value1.next = value2
value2.next = value3
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "true"
// reset all three `handle` pointers to point to nothing
value1 = #:
value2 = #:
value3 = #:
// validate the pointers are indeed pointing to nothing
assert(!value1)
assert(!value2)
assert(!value3)
// despite `value1`, `value2`, and `value3` being reset and falling
// out of scope, the original constructed `value1`, `value2`, and
// `value3` type instances will never be destroyed because they are all
// keeping their lifetimes alive by pointing to each other in a
// circular pointer loop
// `value1` points to itself and `value2`' instance
// `value2` points to `value1`'s instance and `value3`'s instance
// `value3` points to `value1`'s instance
// all of the following circular `handle` pointer prevent
// `MyType` destruction/deallocation
// 1 points to 1 (itself)
// 1 points to 2 points to 1 again
// 1 points to 2 points to 3 points to 1 again
// 2 points to 3 which points to 1 which points to 2 again
// 3 points to 1 which points to 2 which points to 3 again
}
return "I'll be back."
}
The example below creates a circular handle
pointer chain and manually fixes the issue:
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
head : MyType * handle
next : MyType * handle
myValue1 : Integer
myValue2 : String
+++ final : ()() = {
print("This will be displayed three times in this example.")
}
--- final : ()() = {
print("GOOD NEWS! This will be called three times in this example.")
}
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the context's
// allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// the @ operator allocates `value2` dynamically with the context's
// allocator and a `handle` pointer is maintained to another `MyType`
// instance
value2 : MyType * handle @
// the @ operator allocates `value3` dynamically with the context's
// allocator and a `handle` pointer is maintained to another `MyType`
// instance
value3 : MyType * handle @
// setup pointers to the head of the linked list
value1.head = value1
value2.head = value1
value3.head = value1
// setup points to the linked list chain
value1.next = value2
value2.next = value3
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "true"
// break apart any way the pointer relationships could be circular
value1.head = #
value2.head = #
value3.head = #
// reset all three `handle` pointers to point to nothing
value1 = # // `value1`'s instance will be destructed/deallocated (which
// removes the handle pointer to `value2`)
value2 = # // `value2`'s instance will be destructed/deallocated (which
// removes the handle pointer to `value3`)
value3 = # // `value3`'s instance will be destructed/deallocated
// validate the pointers are indeed pointing to nothing
assert(!value1)
assert(!value2)
assert(!value3)
}
return "I'll be back."
}
Pointers qualified as hint
will only contain a valid pointer to a type's instance so long as the original allocated type is not destructed/deallocated. In other words, hint
points will not extend the lifetime of a handle
pointer beyond the last handle
pointer keeping a type's instance alive.
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
head : MyType * hint // head will not be the lifetime of whatever it
// points to alive
next : MyType * handle // next will keep whatever it points to alive
myValue1 : Integer
myValue2 : String
+++ final : ()() = {
print("This will be displayed three times in this example.")
}
--- final : ()() = {
print("GOOD NEWS! This will be called three times in this example.")
}
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the context's
// allocator and a `handle` pointer is maintained
value1 : MyType * handle @
// the @ operator allocates `value2` dynamically with the context's
// allocator and a `handle` pointer is maintained to another `MyType`
// instance
value2 : MyType * handle @
// the @ operator allocates `value3` dynamically with the context's
// allocator and a `handle` pointer is maintained to another `MyType`
// instance
value3 : MyType * handle @
// setup pointers to the head of the linked list
value1.head = value1
value2.head = value1
value3.head = value1
// setup points to the linked list chain
value1.next = value2
value2.next = value3
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "true"
// obtain a `handle` pointer to the head type
headPointer := value3.head as handle
printIfValidPointer(headPointer) // will print "true"
// reset all three `handle` pointers to point to nothing
value1 = #:
value2 = #:
value3 = #:
// validate the pointers are indeed pointing to nothing
assert(!value1)
assert(!value2)
assert(!value3)
// validate the original three instances are still alive
printIfValidPointer(headPointer) // will print "true"
printIfValidPointer(headPointer.next) // will print "true"
printIfValidPointer(headPointer.next.next) // will print "true"
// take the `head` pointer of the `next` pointed type
copyOfWeakHead : MyType * hint = headPointer.next.head
// the `MyType` instance pointed to by the `hint` pointer is still
// alive thus converting the `hint` point to a `handle` pointer will
// obtain a `handle` pointer reference to the original type's instance
printIfValidPointer(copyOfWeakHead as handle) // will print "true"
// the `handle` pointer both point to the same head type's instance
assert((copyOfWeakHead as handle) == headPointer)
// reset the head pointer to point to nothing (which is the only
// pointer keeping all three `MyType` instances alive thus all three
// `MyType` instances are destructed/deallocated)
headPointer = #:
// the `MyType` instance originally pointed to by the copy of the
// `hint` pointer to the head type's instance is now
// destructed/deallocated thus a `handle` pointer obtained from the
// `hint` pointer now points to nothing
printIfValidPointer(copyOfWeakHead as handle) // will print "false"
// all instance of `MyTpe` are already destructed thus nothing further
// occurs when all the values fall out of scope
}
return "I'll be back."
}
Pointers qualified as own
can be transferred to pointers qualified as handle
. Once a transfer is completed, the original own
pointer will point to nothing as the handle
pointer will track the lifetime of the instance. Likewise, pointers qualified as handle
can be transferred to pointers qualified as own
on the condition that no other pointers qualified as handle
points to the same type
's instance otherwise the resulting own
pointer will point to nothing.
print final : ()(...) = {
// ...
}
assert final : ()(check : Boolean) = {
// ...
}
MyType :: type {
myValue1 : Integer
myValue2 : String
}
printIfValidPointer final : ()(pointerToValue : MyType *) = {
if pointerToValue
print("true")
else
print("false")
}
func final : (result : String)() = {
scope {
// the @ operator allocates `value1` dynamically with the context's
// allocator and a `handle` pointer is maintained
value1 : MyType * own @
// `value2` is is a `handle` pointer but the value is initialized to
// point to nothing
value2 : MyType * handle
// `value1` used to `own` the instance but the instance ownership is
// transferred from a `value1` to `value2` which now keeps the
// `MyType` instance alive
value2 = value1
printIfValidPointer(value1) // will print "false"
printIfValidPointer(value2) // will print "true"
assert(value1 != value2)
// transferring a `handle` pointer to an `own` pointer can be
// done so long as no other `handle` pointers exist to the same
// instance (otherwise `value1` would point to nothing)
value1 = value2
// `value2`'s `handle` pointer was automatically reset when ownership
// was exclusively taken over by `value1`
assert(!value2)
printIfValidPointer(value1) // will print "true"
printIfValidPointer(value2) // will print "false"
assert(value1 != value2)
// `value3` is is a `handle` pointer but the value is initialized to
// point to nothing
value3 : MyType * handle
// once again transfer ownership from `value1` to `value2`
value2 = value1
printIfValidPointer(value1) // will print "false"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "false"
// `value3` is a `handle` point but ownership in `value1` was already
// lost thus `value3` will point to nothing
value3 = value1
printIfValidPointer(value1) // will print "false"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "false"
// `value2` and `value3` now point to the same allocated instance
// originally allocated in `value1`
value3 = value2
printIfValidPointer(value1) // will print "false"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "true"
assert(value1 != value2)
assert(value2 == value3)
// `value1` cannot retake ownership from `value2` as another pointer
// to the same instance of `value2` exists thus `value1` cannot take
// exclusive ownership and `value1` will point to nothing
value1 = value2
printIfValidPointer(value1) // will print "false"
printIfValidPointer(value2) // will print "true"
printIfValidPointer(value3) // will print "true"
}
return "I'll be back."
}
Pointers qualified as handle
cannot be directly transferred to a pointer qualified as strong
. The only method by which this transfer can happen is if a handle
pointer is first transferred to an own
pointer and then transferred to a strong
pointer. The vice versa limitation is also true. Pointers qualified as strong
cannot be directly transferred to a pointer qualified as handle
. The only method by which this transfer can happen is if a strong
pointer is first transferred to an own
pointer and then transferred to a handle
pointer.
Transferring to an own
pointer has limitations. Only if a pointer qualified as handle
or strong
is the exclusive reference to the instance of a type can a transfer to an own
pointer occur.
If a handle
pointer will be transferred to a different thread, either a deep
copy of the handle
pointer should be performed or exclusive ownership of the handle should be performed by first transferring the pointer to an own
pointer. By default, handle
pointers allocated using standard allocators (i.e. the sequential allocator). Allocation of a handle
pointer in one thread and then deallocation of a pointer on a different thread may leave memory dangling until a cleanup is performed on dangling allocations on the original allocation thread.
While the standard allocators can be replaced with parallel allocators, the optimized thread-unaware allocators would be replaced by less efficient thread aware counterparts universally (which is often undesirable for efficiency reasons).
The lifetime of
operators can be used to cast a raw pointer to a variable which has a lifetime tied to an original handle
or strong
pointer.
A handle
pointer to a type's instance may contain other types within the instance that share a common lifetime. While the lifetime of these contained types are the same as the container type, only a handle
pointer to the container type may exist (despite both types being considered as a single instance). The lifetime of
operator is especially useful to create a handle
pointer of a contained type from a handle
pointer to a container's type.
Example as follows:
MyType :: type {
value1 : Integer
value2 : String
}
myType : MyType * handle @
// create a pointer to `value` and link the lifetime of `myType` to the pointer
value : Integer * handle = myType.value1 lifetime of myType
// resetting the `myType`'s `handle` pointer will not impact the real lifetime
// of the instance connected to `myType` (as the `value` `handle` pointer will
// keep it's container instance alive)
myType = #
// set the `MyType::value1` to `5` (which is still valid as the
// lifetime of of the original `handle` pointer is kept alive)
value. = 5
While any pointer to any type can be linked to a handle
or strong
pointer using an unsafe lifetime of
operator, a lifetime of
operator can link a pointer to a contained type back to the original handle
or strong
pointer safely by verifying a pointer refers to a memory addresses within the bounds of the allocated handle
or strong
pointer.
An example of a runtime lifetime of
being applied onto a handle
pointer:
A :: type managed {
foo : Integer
}
B :: type {
bar : Integer
a : A
}
C :: type {
weight : Double
}
doSomething final : ()(a : A * handle) = {
// probe `a` to see if it is indeed within a `B` type and if so then
// return a pointer to a B type and create a `handle` pointer from `a`
b := (a outer of B *) lifetime of a
}
function final : ()() = {
value : B * handle @
value.a.foo = 1
value.bar = 2
// link a `handle` pointer to `a` from `value`
a : A * handle = value.a lifetime of value
doSomething(a)
}
The exclusive difference between these operators is safety. An unsafe lifetime of
operator will force a conversion of any raw pointer to link to handle
or strong
pointer even for unrelated pointers. A lifetime of
operator will validate a raw pointer actually points inside the address boundaries of the handle
or strong
pointer. If a pointer to memory is not located in the correct allocation boundary then lifetime of
will return a pointer to nothing. Thus a programmer can choose their tradeoff between safety and speed.
Any pointer to any type can be linked to a handle
or strong
pointer using an unsafe lifetime of
operator. The memory is not checked to see if the memory address being casted resides within the memory space of the original handle
or strong
pointer. The programmer must be careful to use this feature carefully since this function will forcefully adopt the lifetime of a handle
or strong
pointer.
An example of a runtime unsafe lifetime of
being applied onto a handle
pointer:
A :: type managed {
foo : Integer
}
B :: type {
bar : Integer
a : A
}
C :: type {
weight : Double
}
doSomething final : (result : Unknown * handle)(a : A * handle, unknown : Unknown *) = {
// forcefully convert from the lifetime of one pointer to another type
// without conducting any safety checks to ensure the casted pointer lives
// within the memory range of the original allocation
b := unknown unsafe lifetime of a
assert(b)
// ...
return b
}
function final : ()() = {
value : B * handle @
value.a.foo = 1
value.bar = 2
// link a `handle` pointer to `a` from `value`
a : A * handle = value.a lifetime of value
doSomething(a, value)
}
A handle
and hint
pointer contain a pointer to an instance of a type and a pointer to a control block. When a type is allocated for storage in a handle
pointer, a control block is typically reserved with the allocation of a type
's instance.
Since hint
pointers also need control blocks, hint
pointers can keep a memory control block alive and possibly a type
's reserved memory too if a control block and type
are allocated within the same memory block. To clean up allocated memory, reset any hint
pointers to point to nothing after all handle
pointers are gone.
An example handle
pointer content and control block:
/*
HandlePointerControlBlock$(Type) :: type {
strongCount : Integer
[[reserve=(size of Atomic$(Integer)) - size of Integer]]
weakCount : Integer
[[reserve=(size of Atomic$(Integer)) - size of Integer]]
allocator : Allocator *
destructor : ()() *
deallocateType : Unknown *
deallocateControl : Unknown *
type : :: union {
type : $Type
}
}
HandlePointerContents$(Type) :: type {
instance : $Type *
control : HandlePointerControlBlock$($Type) *
}
*/