Several changes landed today:- {$incfile} removed -> replaced by {$embedstr} + {$embedbytes}
- new lock / trylock statements
- thread-static locals (threadstatic / tstatic)
- auto-properties (readonly / writeonly) - beta
- new zeroinit routine modifier
- IDE: Simple Program startup + Project Overrides page
{$incfile} replaced by {$embedstr} and {$embedbytes}Breaking: the
{$incfile} directive is gone, replaced by two focused directives split by output type:
- {$embedstr} - the file becomes a String
- {$embedbytes} - the file becomes an array of byte
Migration is a rename:
{$incfile name 'f'} ->
{$embedstr name 'f'}.
Docs: docs/embed.mdBoth read the file as raw bytes at compile time and bake it into the binary at the directive site - no runtime file I/O, no external dependency. They are directives, not modeswitches, so they work in every
{$mode}. Each has a 2-arg form (
NAME 'path' -> a named
const) and a 1-arg form (
'path' -> a bare value expression, usable inline as an argument or RHS).
program embed_demo;
{$mode unleashed}
{$embedstr greeting 'banner.txt'} // -> const greeting: String
{$embedbytes bytes 'banner.txt'} // -> const bytes: array[N] of byte
begin
WriteLn(Length(greeting), ' bytes'); // 18 bytes (= exact file size)
WriteLn(greeting); // Hello from embed!
WriteLn('first byte = ', bytes[0]); // first byte = 72 ('H')
end.
greeting holds the file's bytes byte-for-byte (
Length = file size);
bytes is the same data typed as
array of byte for binary APIs (hashing, decoders, protocols). Use
$embedstr for text/buffers,
$embedbytes when an API demands
array of byte.
Notes- 2-arg forms expand to a const (must sit where a const is valid); 1-arg forms expand to a bare value - {$embedstr 'f'} is a String expression, {$embedbytes 'f'} a [$xx,..] array literal you can pass straight to a function or compare inline.
- Gotcha: var x := {$embedbytes 'f'}; infers array of LongInt (4-byte elements), not array of byte. Pin it with the 2-arg form, an explicit : array of byte, or a typed parameter.
- Path resolution matches {$I}: absolute verbatim, relative searched in the source dir then the include paths.
New statements: lock / trylockOn by default in
{$mode unleashed}. Outside:
{$modeswitch lock}.
Docs: docs/lock.mdTwo statements that serialize access across threads on top of FPC's
TRTLCriticalSection, built so the acquire/release pairing can never be forgotten - the body is wrapped in a
try..finally, so the lock is released on normal exit,
exit/
break/
continue/
goto, or a propagating exception. The verb split is the whole point:
lock blocks until acquired and cannot fail (no
else);
trylock may miss, so its
else branch is mandatory, with an optional
wait N (milliseconds) bounded retry.
program lock_demo;
{$mode unleashed}
var counter: Integer;
procedure Bump; begin lock(counter) do Inc(counter); end; // per-variable lock
procedure Bump2; begin lock(counter) do Dec(counter); end; // same hidden CS as Bump
begin
counter := 0;
Bump; Bump; Bump; Bump2;
WriteLn('counter = ', counter); // counter = 2
lock do WriteLn('one thread at a time here'); // per-callsite lock
trylock(counter) do Inc(counter) else WriteLn('busy');
WriteLn('counter = ', counter); // counter = 3
trylock wait 50 do WriteLn('acquired') else WriteLn('timed out'); // bounded wait
end.
How the lock is chosen- lock(v) - every lock(v)/trylock(v) across the whole program shares one hidden critical section for v (protects the data).
- lock do - a hidden CS per source position (protects that sequence of instructions).
- lock(a, b, ...) - multi-lock; targets are entered in a fixed name-sorted order at every site, so the AB-vs-BA deadlock is impossible.
- If a target is itself a TRTLCriticalSection it's used directly (your lifetime to manage) - the escape hatch. Auto targets must be a global var or a class var. All locks are recursive (re-entrant).
New feature: thread-static locals (threadstatic / tstatic)On by default in
{$mode unleashed}. Outside:
{$modeswitch threadstatic}.
Docs: docs/thread-static.mdA block-local variable with program lifetime, like
static - but
per thread: each thread gets its own copy, initialized once per thread on first reach. Inline form (
threadstatic name := expr; anywhere in a body) and declaration-section form (before the body, like
var/
static).
tstatic is a short, drop-in alias for
threadstatic.
program tstatic_demo;
{$mode unleashed}
function NextId: Integer;
begin
threadstatic next := 1000; // per-thread; each thread starts fresh at 1000
Result := next;
Inc(next);
end;
function NextTick: Integer;
tstatic // short alias, declaration-section form
n: Integer = 0;
begin
Inc(n);
Result := n;
end;
begin
WriteLn(NextId, ' ', NextId, ' ', NextId); // 1000 1001 1002
WriteLn(NextTick, ' ', NextTick, ' ', NextTick); // 1 2 3
end.
In one thread these behave like a call-persistent counter; spawn more threads and each keeps its own independent
next /
n.
Notes- Cost: on win32/win64 the threadvar relocate is inlined to a TEB read; with the address hoisted out of a loop (-O2) an access costs the same as an ordinary global. (The old call-per-access path was several times slower.)
- vs static: a static const-init folds into the data segment; thread-static can't (TLS has no per-thread template), so a non-zero initializer becomes a per-thread guarded assignment - one branch on first use per thread, free after. An all-zero init (0/nil/'') skips even that.
- static = one instance shared by all threads; threadstatic = one per thread. Scope is the declaring routine only.
New feature: auto-properties - readonly / writeonly (beta)On by default in
{$mode unleashed}. Outside:
{$modeswitch autoproperties}.
Docs: docs/auto-properties.mdA property declared with a type but
no read/
write clause makes the compiler synthesize a hidden
strict private backing field and bind the property straight to it (
read FName write FName). No getter/setter method - identical codegen to a hand-written field-backed property, zero overhead.
readonly/
writeonly narrow the access;
= const seeds the field with a default.
program autoprop_demo;
{$mode unleashed}
type
TPerson = class
property Name: String; // -> strict private FName; read/write
property Id: Integer; readonly; // -> read FId only
property Tag: String = 'x'; // initializer seeds the backing field
constructor Create(aId: Integer);
end;
constructor TPerson.Create(aId: Integer);
begin
FId := aId; // backing field reachable inside methods
end;
var p: TPerson;
begin
p := TPerson.Create(42);
p.Name := 'Bob';
WriteLn(p.Name, ' ', p.Id, ' ', p.Tag); // Bob 42 x
p.Free;
end.
Notes- Backing field = F + property name; change the prefix with {$autopropprefix PREFIX} or --autopropprefix=PREFIX
- Works on classes, objects and records; a class property -> a shared class var; a published auto-property is RTTI-complete.
- Indexed properties still need explicit accessors; initializers apply only to instance properties of classes/objects.
- Beta - the newest feature in this batch and still stabilizing. Try it, report anything odd.
New modifier: zeroinit - zero-fill all locals on entryAvailable in
{$mode unleashed}. No separate modeswitch.
Docs: docs/zeroinit.mdprocedure foo; zeroinit; zero-initialises every local variable at function entry - the compiler inserts an implicit
Default(T) for each local, in declaration order, before any statement runs. FPC already clears managed locals (strings, dynamic arrays, interfaces);
zeroinit extends that to the simple ones (
Integer, plain records, static arrays) that otherwise carry whatever the stack left behind. Add a new local and it's zeroed too - no explicit list to maintain.
function Sum: Integer; zeroinit;
var
a, b, c: Integer; // all zeroed on entry - no explicit := 0 needed
begin
Inc(a, 10);
Inc(b, 20);
Inc(c, 30);
Result := a + b + c; // 60 (a, b, c started at 0)
end;
Handy for defensive code, FFI shims, code-gen output and quick prototypes, where a deterministic frame beats hunting "why is this 0 sometimes and garbage other times".
Notes- Applies to standalone routines, methods, constructors and destructors. Mutually exclusive with external / interrupt / assembler - those have no Pascal body to inject into.
- Result is treated as a local and zeroed too, so an unmanaged return type is deterministic before you assign it.
- Gotcha: a local declared with an inline anonymous compound type (var a: array[0..3] of Integer) is silently skipped - name the type (type TArr4 = array[0..3] of Integer) to have it zeroed.
- Pure sugar: it injects ordinary zero-assignments - no calling-convention or stack-frame change, no runtime helper.
IDE: Simple Program startup + Project "Overrides" pageTwo IDE additions (screenshots below).
IDE Options -> Environment -> IDE Startup -> Simple Program. Three new settings so a new Simple Program starts with your own names instead of the fixed defaults:
Default app name sets the
program name and
Main proc name the main routine, so File -> New -> Simple Program builds the skeleton the way you like it. An
Include {$mode unleashed} checkbox is there too, if you want it.
Project Options -> Overrides. A new page that surfaces the Unleashed binary-metadata and hardening switches in the GUI instead of hand-typing custom options:
- FPC signature (--fpcsignature) - override it, or with Set empty drop the .fpc.version ident string entirely.
- Override linker version / Override OS version (--linkerversion / --osversion) - PE-header fields, Windows targets only.
- Strip RTTI (--striprtti) + a whitelist (--rttiexpose, wildcards allowed) - nulls type-name strings in RTTI/VMT.
- Auto-properties prefix (--autopropprefix, default F) - the backing-field prefix for the feature above.
Docs: docs/binary-metadata.md,
docs/strip-rtti.md