Life-Time Strategies

Life-Time Strategies are a way to control how the life-time of objects is managed, i.e. to determine how an object gets generated and released.

By default, each Island sub-platform and/or Object Model defines a default life-time strategy to be used, and it is very rare that developers would everneed to override that in normal application code.

To explicitly specify a life-time strategy for an individual field or variable, the Oxygene lifetimestrategy keyword can be used as a type modifier, similar to how Storage Modifiers are used in the Cocoa platform. The keyword takes the name of a life-time strategy as parameter in parentheses.

var x: lifetimestrategy(Manual) String;

Since this is longwinded to type, and only available in the Oxygene language, Island RTL provides a generic alias for each life-time strategy, that can be used to wrap an object, instead:

var x: Manual<String> := ...;
Manual<String> x = ...;
let x: Manual<String> = ...
Manual<String> x = ...;
Dim x As Manual(Of String) = ...

For the example above, the Manual<T> alias is defined in Island RTL like this:

type 
  Manual<T> = public lifetimestrategy(Manual) T;

Available Life-Time Strategies

Island RTL predefines these strategies:

  • Manual: When used, allocates the object on the heap without any life-time management. Objects need to be explicitly freed by calling Manual.FreeObject(), like they would be in more classic development environments such as C or Delphi.
  • RC: Uses reference counting for the object and frees it automatically when the last reference to the object goes to nil/null.
  • BoehmGC: Uses the Boehm Garbage Collector to allocate and cleanup the objects. This is the default for native Island objects, on all platforms other than WebAssembly.
  • ForeignBoehmGC: Same as BoehmGC but used when a Garbage-Collected reference is stored in a non-garbage-collected object, like for example a Cocoa class. This ensures it's properly taken in account in both GC and the Referende Counting life-cycle of the containing object.
  • SimpleGC: A simple single-threaded garbage collector implementation for WebAssembly, used as the default on that sub-platform.
  • ObjcStrong and ObjcWeak: Automatically used for the Cocoa class model when using strong and weak references in cocoa using the respective Storage Modifiers
  • COM: Used for COM Interface support, to manage life-time via calls to AddRef/Release calls to the IUnknown interface.

Lifetime strategies are not compatible with each other without an explicit conversion operator defined on the strategies themselves. Right now, only the ObjcStrong/ObjcWeak strategies support this.

Aspects

Three special Aspects are provided to work with life-time strategies:

  • DefaultObjectLifetimeStrategy: defines the default lifetime strategy for a project. Only 1 of these can be applied, and the Island RTL binary does ths for you.
  • GCSkipIfOnStack: skip this lifetime strategy's management if the target can be proven to remain on the stack. Useful for scanning garbage collectors.
  • LifetimeStrategyOverride: Globally override the lifetime of a pointer type to use another strategy. This can be used, for example, to turn an opaque Win32 pointer type such as HString into a native island type managed by the specified lifetime strategy.

Implementing Life-Time Strategies

Generally, developers will not have to implement a strategy themselves, but Elements does provide the provision to do so, if needed.

Life-Time Strategies are Records/Structs that implement the ILifetimeStrategy<T> and a Finalizer and Constructors, if needed. They should have a single field of type IntPtr, that can hold the pointer for the maintained object. The compiler will do the work of casting this to the real type.

For example, the RC Reference-Counting Life-Time Strategy is defined like such:

type
  RC<T> = public lifetimestrategy (RC) T; // Alias
  
  RC = public record(ILifetimeStrategy<RC>)
  private 
    fValue: IntPtr;
  public
  
    class method &New(aTTY: ^Void; aSize: IntPtr): ^Void;
    begin 
      result := ^Void(malloc(aSize + sizeOf(^Void)));
      ^UIntPtr(result)^ := 1;
      result := result + sizeOf(^Void);
      ^^Void(result)^ := aTTY;
      memset(^Byte(result) + sizeOf(^Void), 0, aSize - sizeOf(^Void));
    end;
    
    class method &Copy(var aDest, aSource: RC); 
    begin
      var lSrc := aSource.fValue;
      if lSrc = 0 then exit;
      InternalCalls.Increment(var ^IntPtr(lSrc)[-1]);
      aDest.fValue := lSrc;
    end;
    
    constructor &Copy(var aSource: RC);
    begin
      var lSrc := aSource.fValue;
      if lSrc = 0 then exit;
      InternalCalls.Increment(var ^IntPtr(lSrc)[-1]);
      fValue := lSrc;
    end;
    
    class operator Assign(var aDest: RC; var aSource: RC);
    begin
      Assign(var aDest, var aSource);
    end;
    
    class method Assign(var aDest, aSource: RC); 
    begin
      if (@aDest) = (@aSource) then exit;
      var lInst := aSource.fValue;
      var lOld := InternalCalls.Exchange(var aDest.fValue, lInst);
      if lOld = lInst then exit;
      if lInst <> 0 then begin 
        InternalCalls.Increment(var ^IntPtr(lInst)[-1]);
      end;
      if lOld <> 0  then begin 
        if InternalCalls.Decrement(var ^IntPtr(lOld)[-1]) = 0 then  
          FreeObject(lOld);
      end;
    end;
    
    finalizer;
    begin
      var lValue := InternalCalls.Exchange(var fValue, 0);
      if lValue = 0 then exit;
      var p := InternalCalls.Decrement(var ^IntPtr(lValue)[-1]);
      if p = 0 then 
        FreeObject(p);    
    end;

    class method Init(var Dest: RC); empty;
    
    class method Release(var Dest: RC); 
    begin 
      var lValue := InternalCalls.Exchange(var Dest.fValue, 0);
      if lValue = 0 then exit;
      var p := InternalCalls.Decrement(var ^IntPtr(lValue)[-1]);
      if p = 0 then 
        FreeObject(p);
    end;
    
    class method FreeObject(aObj: IntPtr); private;
    begin 
      if aObj = 0 then exit;
      try {$HIDE W58}
        InternalCalls.Cast<Object>(^Void(aObj)).Finalize;
        {$SHOW W58}
        free(^Void(aObj - sizeOf(IntPtr)));
      except 
      end;
    end;
    
  end;
  • New gets called when creating a new object instance
  • Init gets called when a variable or field is set to the default 'not assigned' value
  • The Copy constructor and method both perform the same, but the constructor is required when using ComGC directly. This is called when creating a new instance based on another, and it ignores the value in the destination and overwrites it without any action to release it.
  • The Assign operator and method work the same as Copy, except they releases the old value of the object before assigning.
  • Release and/or the Finalizer are called when releasing the object.