Blog › ICP guides
Oxygene developer on retainer: property implicit backing fields, multi-target compilation, nullable types, and Oxygene .NET programming on monthly retainer
November 11, 2026 · ~16 min read
An Oxygene program managing a configuration class was producing four wrong property reads per run. The class had a Config property originally declared with Oxygene’s implicit backing field syntax — a property declaration without explicit accessor methods, which causes the Oxygene compiler to auto-generate a backing field with a compiler-chosen name. When the team refactored the property to use an explicit backing field named fConfig, they added the fConfig field declaration and updated the setter (set_Config) to write fConfig := value. The setter compiled and ran correctly: every write reached the new explicit backing field. But the getter (get_Config) was not updated: it still contained result := self.Config, which in Oxygene resolved to the compiler-generated implicit backing field rather than to fConfig. Because the property now had explicit accessor methods, Oxygene still generated an internal backing field for the property — this auto-generated field started empty. Every read from the Config property went through the getter, which read the empty auto-generated field rather than the fConfig field where all the writes had gone. Four property reads per run returned the empty default string instead of the configured value. The Oxygene developer on retainer diagnosed the backing field split: the setter and getter were referencing two different storage locations, one of which was never written. The fix updated the getter to result := fConfig, making both accessor methods consistent on the same explicit backing field. Wrong property reads per run: 4 → 0.
The work log entry read “fixed Config property reads, 13h.” It names the symptom and duration. It cannot explain to a client why Oxygene generates an implicit backing field for a property even after explicit accessor methods are added (Oxygene’s property system has two modes: the auto-property mode where property Name: String; with no explicit read/write clause generates a backing field and trivial get/set accessors automatically; and the explicit accessor mode where property Name: String read get_Name write set_Name; pairs the property with named methods — but in certain versions of Oxygene and certain property declaration patterns, the compiler continues to allocate an internal backing field slot for the property even when explicit accessors are provided, because the backing field allocation is tied to the property declaration in the compiler’s data model rather than to the presence or absence of accessor method bodies), why the getter writing result := self.Config reads from the auto-generated slot rather than from fConfig (accessing a property by name inside one of its own accessor methods is a potential recursive accessor call in some languages; Oxygene resolves it to the backing storage of the property at the method-call level, but the backing storage at that level is the compiler-generated slot, not the programmer-declared fConfig field, producing a silent misdirection), or why the retainer work required auditing every other property in the same class and in related classes for the same implicit/explicit backing field inconsistency pattern (a class with ten properties that had been through a similar refactoring might have three or four properties with getters still referencing implicit fields, each one a silent wrong-value source for a different configuration key; identifying and fixing all instances requires systematic property-by-property audit, not just fixing the reported symptom). The 13 hours of getter/setter backing field reference audit, property declaration consistency analysis, and related-class survey are not visible in the diff beyond corrected getter method bodies.
Oxygene properties: implicit backing fields, explicit backing fields, and property block syntax
Oxygene’s property system is the language’s most common source of retainer work because it has two distinct modes that look similar in source code but have different storage semantics. Auto-property mode: property Name: String; — no explicit read or write clause. The Oxygene compiler generates a backing field (with an internal compiler-chosen name), a getter that returns the field value, and a setter that assigns to the field. The programmer never writes accessor method bodies; the compiler provides them implicitly. This mode is syntactically compact and correct as long as the programmer understands that storage is in the compiler-generated field. Explicit accessor mode: property Name: String read GetName write SetName; — explicit read and write clauses naming accessor methods. The programmer declares the accessor method bodies explicitly. Storage is in whatever field the accessor methods reference — explicitly declared: private fName: String; — and the accessor methods must consistently reference the same field for reads and writes to agree.
The Oxygene property block syntax is an alternative to named accessor methods. Instead of declaring separate GetName and SetName methods, the property declaration can contain an inline block: property Name: String read (result := fName) write (fName := value);. The read clause contains an expression that is evaluated and returned as the property value; the write clause contains a statement executed on assignment, with value bound to the assigned value. This inline syntax keeps the backing field reference in the same declaration as the property, making backing field consistency easier to verify at a glance: both read and write clauses are in the same source line, referencing the same fName identifier. The property block syntax does not prevent the implicit/explicit mismatch bug if the programmer mixes a property block declaration with a separately declared explicit backing field and does not verify that the field names match exactly. Auto-property vs explicit-accessor selection for a given property should be a deliberate design choice based on whether the accessor needs to perform any logic beyond simple field access (validation, notification, lazy initialization); pure storage with no accessor logic is best served by auto-property syntax, which eliminates the getter/setter-vs-backing-field consistency surface area entirely.
Read-only and write-only properties in Oxygene: a property with only a read clause is read-only (no setter; assignment to the property is a compile error). A property with only a write clause is write-only (no getter; reading the property is a compile error). Write-only properties are rare but used for security-sensitive fields like passwords where the value should be stored (e.g., hashed) but never read back in plaintext. Property access modifiers: property Name: String; protected write; declares a property with public read access and protected write access — external code can read the property but only the class and its subclasses can write it. The asymmetric access modifier pattern is used for properties that represent observable state (public read for the observer, protected write for the owning class). Lazy properties in Oxygene: a property can be declared lazy with compiler annotation, causing the backing field to be initialized from the getter body on first access rather than at construction time; lazy initialization requires thread-safety consideration if the lazy property is accessed concurrently from multiple threads without synchronization.
Oxygene class design: class/record types, sealed/abstract/partial, method dispatch, and protocols
Oxygene distinguishes class types from record types. A class type is a reference type: variables of a class type hold references to heap-allocated instances; assignment copies the reference, not the instance; nil is a valid value for a class type variable. A record type is a value type: variables of a record type hold the instance value directly; assignment copies the entire record; record types cannot be nil (they always have a value). The class/record distinction maps directly to .NET reference types and value types, Java objects and primitives (though Java has no first-class value types), and Cocoa reference-counted objects vs value types. Choosing the wrong kind for a given abstraction is a common design error: a type that represents a small immutable value (a point, a color, a version number) is better as a record (copied by value, no nil, no heap allocation per use), while a type that represents a large shared mutable entity (a connection, a session, a document) is better as a class (referenced by pointer, shared by multiple holders, nil-able for optional presence).
Method dispatch in Oxygene: the default method is non-virtual (static dispatch, resolved at compile time to the declared type). virtual declares a method overridable by subclasses. abstract declares a method that the current class does not implement and that subclasses must implement; a class with any abstract method is implicitly abstract (cannot be directly instantiated). override in a subclass declares that the method overrides the named virtual or abstract method from the superclass. final on an override prevents further overriding by subclasses of the subclass. reintroduce is Oxygene’s (inherited from Delphi Pascal) mechanism for non-virtual hiding: a subclass can reintroduce a method with the same name that is not an override of the superclass method but a new non-virtual method that hides the superclass method when accessed through the subclass type. Sealed classes: sealed class prevents subclassing of the entire class. Partial classes: partial class allows the class declaration to be split across multiple files, with the compiler combining all partial declarations at compile time; this is used for code generation scenarios (one partial declaration written by a developer, another generated by a tool). Partial class members declared in one file are visible in all other partial declarations of the same class.
Protocols in Oxygene (equivalent to interfaces in Java/.NET): a protocol declaration specifies a set of method signatures and property declarations that any implementing type must provide. class MyClass implements IProtocol; declares that MyClass satisfies the protocol’s interface contract. Oxygene protocols correspond to .NET interfaces, Java interfaces, and Objective-C protocols (on the Cocoa target). Protocol default implementations: Oxygene supports extension methods on protocols, allowing a protocol to provide default implementations for some methods that implementing types can optionally override. Operator overloading in Oxygene: a class or record can define custom behavior for built-in operators (+, -, *, /, =, <>, <, >, <=, >=) by declaring class methods named operator +, etc. The Oxygene compiler maps operator expressions to the corresponding operator method when the types involved have operator overloads declared. Type-covariant return types: a method override in Oxygene can narrow the return type to a subtype of the superclass method’s return type (the return type is covariant with the class hierarchy). This allows a factory method declared in a base class to be overridden in a subclass to return the subclass type, making the type system aware of the more specific return type without requiring the caller to cast.
Oxygene multi-target compilation: .NET, Cocoa, Java/Android, and Island native
Oxygene’s headline feature is that the same Pascal-syntax source can compile to four distinct platform targets using the same RemObjects Elements compiler toolchain. The .NET/Mono target produces CIL (Common Intermediate Language) bytecode that runs on the .NET runtime; types are mapped to System.Object, System.String, System.Collections.Generic.List<T>, and other System namespace types. The Cocoa target produces Objective-C-compatible class implementations that compile to native ARM/x86 code for iOS, macOS, watchOS, and tvOS; types are mapped to NSObject, NSString, NSMutableArray, NSDictionary, and other Foundation framework types. The Java/Android target produces JVM bytecode; types are mapped to java.lang.Object, java.lang.String, java.util.ArrayList, and other java.* types. The Island target produces native code for a range of platforms (Windows, Linux, macOS, iOS, Android, Raspberry Pi) without a managed runtime — Island is RemObjects’ own lightweight runtime providing garbage collection, threading primitives, and a minimal standard library.
Mapped types in Oxygene: a mapped type declaration tells the compiler that a given Oxygene type name maps to a specific platform-native type. __mapped class NSString is mapped to Foundation.NSString; makes NSString a synonym for the Cocoa Foundation.NSString on the Cocoa target. Platform-independent Oxygene code uses the Elements standard library types (RTL.String, RTL.List<T>, RTL.Dictionary<TKey, TValue>) that are automatically mapped to the appropriate platform type for each target. Writing portable multi-target Oxygene code requires using RTL types rather than platform-specific types wherever possible, reserving platform-specific code for blocks guarded by IF TARGET .NET, IF TARGET COCOA, IF TARGET JAVA, or IF TARGET ISLAND conditional compilation directives. Multi-target code that accidentally uses a .NET-specific type (e.g., System.Threading.CancellationToken) without a conditional guard compiles correctly for .NET but fails to compile for Cocoa, Java, and Island targets. Retainer work on multi-target compilation involves systematic platform-specific API audit: for each platform-specific type or method reference in the codebase, either replace with an RTL equivalent or wrap in a conditional compilation block with platform-specific implementations for each target.
Delphi Prism and Oxygene history: Oxygene began as a collaboration between RemObjects Software and Embarcadero Technologies (the owner of Delphi), released in 2007 as Delphi Prism — a Pascal-syntax language for .NET, targeting the market of Delphi developers who wanted to write .NET applications with Pascal syntax. The collaboration ended around 2012; RemObjects continued developing the language as Oxygene independently, adding the Cocoa and Java targets and eventually the Island target. The Delphi Prism name is still encountered in legacy codebases and project files from the 2007–2012 period; retainer work on Delphi Prism codebases involves the additional task of migrating project files and syntax from the Delphi Prism era to current Oxygene, which has changed several syntax forms (especially around generics, async/await, and type declarations). The Oxygene retainer community overlaps significantly with the Delphi retainer community: many Oxygene engagements are Delphi-to-.NET or Delphi-to-multi-target migrations, requiring expertise in both languages and their differing approaches to the same Pascal-origin type system.
Oxygene nullable types, null-conditional operators, and async/await
Oxygene’s nullable types follow the T? pattern: var x: String? declares a nullable String variable that can hold either a String value or nil. Non-nullable types (declared without ?) cannot be assigned nil; the compiler generates a warning or error (depending on compiler settings) if a non-nullable variable might receive nil. The assigned(x) function tests whether x is non-nil; it returns true if x holds a value and false if x is nil. Using assigned() before dereferencing a nullable is the idiomatic null-safety pattern in Oxygene. Checking if assigned(x) then x.Method(); is the safe access pattern. Direct access x.Method() without an assigned() check compiles without error but causes a nil dereference at runtime on Cocoa and Island targets; on .NET and Java targets, it raises a NullReferenceException or NullPointerException.
The null-conditional operator ?. in Oxygene: x?.Field evaluates to nil if x is nil, or to x.Field if x is non-nil. This short-circuits the dereference at the ?. point, propagating nil through the expression. x?.Field?.SubField?.Method() chains null-conditionals through a property access chain; the entire expression evaluates to nil if any step is nil. The null-coalesce operator ??: x ?? defaultValue evaluates to x if x is non-nil, or to defaultValue if x is nil. Combining: x?.Field ?? "none" reads the Field property if x is non-nil, and falls back to the string "none" if either x is nil or Field is nil. The ?. and ?? operators are Oxygene’s mechanism for writing null-safe expression chains without if/then/else nesting; they correspond to C#’s ?. and ?? operators and Swift’s optional chaining and nil-coalescing. Nullable value types: Int32? declares a nullable 32-bit integer. On .NET, this maps to Nullable<Int32>; accessing .Value on a null Nullable raises an exception. Oxygene’s assigned() function handles nullable value types correctly: if assigned(count) then Process(count) is safe; the compiler knows count is non-nil inside the if branch.
Async and await in Oxygene: a method declared async method DoWork: Future<String> is asynchronous and returns a Future<T> (Oxygene’s name for what .NET calls Task<T>, Java calls CompletableFuture<T>, and Cocoa calls dispatch async with a completion handler). Inside an async method, await Expression suspends the current method until the awaited Future completes, then resumes with the completed value. The async/await syntax allows sequential-looking code that executes asynchronously without blocking the calling thread. A method that returns Future<T> can be called with await from another async method; the call site awaits the result and the calling async method suspends until the Future completes. Calling a Future-returning method without await starts the asynchronous operation but does not wait for it; the Future object can be retained and awaited later. Error handling in async methods: exceptions thrown inside an async method body are captured in the returned Future; awaiting a Future that holds an exception re-raises it at the await site, where it can be caught with a normal try/except block. Platform mapping: on .NET, Future<T> maps to System.Threading.Tasks.Task<T>; on Java, to java.util.concurrent.CompletableFuture<T>; on Cocoa, async methods interact with Grand Central Dispatch through RemObjects’ platform binding layer.
How HourTab tracks Oxygene developer retainer hours
Oxygene retainer work shares the invisible-work problem common to all language engineering retainers, compounded by Oxygene’s most frequent retainer task — property system debugging — producing diffs whose surface area is a single corrected line in a getter method body while the diagnostic path required understanding the compiler’s implicit backing field generation behavior, the property block vs named accessor interaction, and the Delphi Prism vs current Oxygene syntax evolution. A backing field stale-read fix is a diff with one getter method corrected; the value is elimination of all wrong configuration reads, a consistent getter/setter storage model, and a correct understanding of Oxygene’s auto-property vs explicit-accessor property modes that prevents the same misdirection in future property refactors. A multi-target compilation repair is a diff with platform-conditional blocks wrapping platform-specific API calls and RTL type substitutions replacing platform-specific type references; the value is a codebase that compiles correctly for all four targets without maintenance divergence, and a documented platform compatibility policy that future additions can follow. A nullable chain audit that adds assigned() guards before all dereferences of T? values is a diff with guards scattered across all access sites; the value is elimination of nil dereference crashes at runtime on Cocoa and Island targets, a semantically correct nil-propagation model throughout the codebase, and a source code that communicates nullable intent clearly through ?. and ?? syntax to future readers.
HourTab gives Oxygene developers a public retainer-hours URL they send to clients — typically Delphi migration teams moving Win32 Delphi codebases to .NET or multi-target Oxygene, cross-platform Pascal shops building applications for Apple, Android, and Windows from a single codebase, and RemObjects Elements teams working on Island native targets — at the start of an engagement. For Oxygene retainers, each work log entry should name the mechanism (implicit vs explicit backing field; getter/setter reference consistency; property block read/write clause; class vs record type selection; sealed/abstract/partial hierarchy design; virtual/override/abstract/final dispatch; protocol definition; multi-target .NET/Cocoa/Java/Island compilation; mapped type resolution; IF TARGET conditional block; T? nullable declaration; assigned() non-nil guard; x?.field null-conditional chain; x ?? default coalesce; async method declaration; await expression; Future<T> return type), the specific property name and the backing field mismatch, and the before/after metric. Oxygene retainers are often compared to Delphi developer retainers for Pascal-syntax language engineering, and to Swift developer retainers for multi-target Apple-platform development. HourTab’s work log makes the property system diagnosis, multi-target compatibility audit, and nullable chain design visible to clients who would otherwise see only the symptom — wrong property reads or runtime crashes — and not understand why the fix required understanding Oxygene’s implicit backing field generation behavior and the accessor method naming conventions that govern which storage slot a getter reads.
Track Oxygene developer retainer hours without the status emails
HourTab gives Oxygene developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Oxygene developer retainers
What does an Oxygene developer on retainer typically do?
An Oxygene developer on monthly retainer covers four principal service areas: property system design (implicit vs explicit backing field audit; getter/setter reference consistency; property block read/write clauses; read-only/write-only property design; lazy property initialization); class hierarchy design (class vs record type selection; sealed/abstract/partial design; constructor/finalizer lifecycle; virtual/override/abstract/final dispatch; protocol definition; type-covariant returns; operator overloading); multi-target compilation (.NET/Mono, Cocoa, Java/Android, Island native target compatibility; mapped type resolution; IF TARGET conditional blocks; RTL type usage for portability); and nullable and async design (T? nullable declarations; assigned() non-nil guards; x?.field null-conditional chains; x ?? default coalesce; async method declarations; await expressions; Future<T> return type pipelines).
What Oxygene work is most commonly underlogged in a retainer?
Implicit backing field stale-read repair (Config property refactored from auto-property to explicit fConfig backing field; setter updated to fConfig := value but getter still read self.Config auto-generated implicit slot; 4 wrong reads/run; restructured getter to result := fConfig; wrong reads: 4/run → 0; 13–22 hrs invisible in getter/setter backing field reference audit), multi-target compilation breakage (method using .NET-specific type failed to compile for Cocoa and Java targets; restructured to IF TARGET blocks and RTL type substitutions; compile failures: 3 targets → 0; 10–18 hrs invisible in target compatibility audit), and nullable dereference repair (T? return value dereferenced without assigned() guard; 5 nil dereference crashes/day; added assigned() guards; crashes: 5/day → 0; 9–15 hrs invisible in nullable chain analysis).
What are typical Oxygene developer retainer rates?
Entry-level Oxygene developers (1–2 years, property declarations, basic class hierarchies, constructor/destructor lifecycle, simple .NET development) bill at $65–$110/hr. Mid-level Oxygene engineers (2–4 years, multi-target compilation, mapped types, nullable T? patterns, async/await Future<T>, protocol design, implicit backing field mechanics) bill at $105–$185/hr. Senior Oxygene architects (4–8 years, full multi-target codebase architecture, complex type-covariant hierarchy design, Island native compilation, Delphi-to-Oxygene migration work) bill at $160–$280/hr. Monthly retainer ranges: $1,800–$5,000/mo advisory (15–25 hrs), $7,000–$18,500/mo for full Oxygene platform engagements.
What should an Oxygene developer retainer agreement include?
An Oxygene developer retainer agreement should specify: property system scope (implicit vs explicit backing field audit; getter/setter reference consistency; property block syntax design; read-only/write-only property; lazy initialization); class hierarchy scope (class vs record type selection; sealed/abstract/partial design; constructor/finalizer lifecycle; virtual/override/abstract/final dispatch; protocol definition; type-covariant returns; operator overloading); multi-target scope if applicable (.NET/Mono, Cocoa, Java/Android, Island compatibility; mapped type resolution; IF TARGET conditional blocks; RTL type usage; Delphi migration); nullable and async scope (T? declarations; assigned() guards; x?.field chains; x ?? coalesce; async/await; Future<T> pipelines); and hour logging format (property name; backing field type and change; before/after error metric; Oxygene version; target platform).
How should Oxygene developer retainer hours be logged?
Log each Oxygene retainer session with: advisory category (implicit backing field stale-read audit; getter/setter backing field reference consistency; property block read/write clause design; sealed/abstract/partial class hierarchy; constructor/finalizer lifecycle; virtual/override/abstract/final dispatch; protocol definition; multi-target .NET/Cocoa/Java/Island compilation; mapped type platform resolution; IF TARGET conditional block design; T? nullable declaration; assigned() non-nil guard; x?.field null-conditional chain; x ?? coalesce; async method; await expression; Future<T> return type), the specific property name and the implicit/explicit backing field mismatch (Config property getter referenced self.Config auto-generated implicit field after fConfig explicit field added; setter wrote fConfig; getter read implicit field returning empty string; 4 wrong reads/run; restructured getter to result := fConfig; wrong reads: 4/run → 0), and the before/after observable metric. Include Oxygene version and target platform, whether the engagement was a Delphi migration, and whether the fix required backing field reference correction, platform-conditional restructuring, mapped type substitution, nullable guard addition, or async pattern redesign.