New feature: Flexible Array MembersModeswitch
flexiblearrays, on by default in
{$mode unleashed}.
C99-style flexible array members - declare a record with a variable-length tail using empty brackets as the last field. The record header has a fixed size, the tail extends as far as the allocation says it does, and
sizeof(rec) reports only the fixed part.
The basicstype
PMessage = ^TMessage;
TMessage = packed record
code: integer;
length: integer;
data: array[] of byte; // flexible array member
end;
The record and its tail live in one block. You allocate the fixed part plus payload in a single
GetMem, write the trailing data through the FAM field by index, and free the whole block in one
FreeMem:
var
msg: PMessage;
i: integer;
begin
GetMem(msg, sizeof(TMessage) + 1024);
msg^.code := 42;
msg^.length := 1024;
for i := 0 to 1023 do
msg^.data[i] := byte(i);
// ... use msg ...
FreeMem(msg);
end;
sizeof(TMessage) returns 8 here (just
code and
length). The FAM contributes nothing to the static size, so the math at the call site is the obvious
sizeof(rec) + payload, with no off-by-one for a phantom one-element tail.
Memory layout +----------+----------+----------+ ... +----------+
GetMem ---> | code (4) | length(4)| data[0] | | data[N] |
+----------+----------+----------+ ... +----------+
^ ^
msg msg^.data
|<-- sizeof(rec) -->|<-- payload bytes -->|
The FAM starts at the offset that natural alignment gives the element type after the last fixed field. Padding is inserted automatically, exactly like for any other field.
Why a FAM and not the alternativesThree patterns are commonly used today; each loses something the FAM keeps:
- data: array[0..0] of byte (the C "struct hack") - {$rangechecks on} rejects every access past index 0, sizeof is one element too large, padding is implicit.
- data: PByte to a separate buffer - two allocations, two frees, an extra indirection on every access, breaks the single-block layout used by Win32 structures.
- case integer of 0: (data: array[0..high(integer)-X] of byte) - sizeof rolls over, high() lies, the compiler has no idea what the actual extent is.
The FAM gives you the inline layout, honest
sizeof, working range-check setting, and a single allocation in one feature.
RestrictionsEnforced at parse time with dedicated error messages (
parser_e_fam_*):
- The FAM must be the last field of the record. No fields can follow.
- The record must have at least one preceding field.
- Only one FAM per record, and only one identifier per FAM declaration (a, b: array[] of byte is rejected).
- A FAM is allowed only as a plain instance field of a plain record - not in class, object, class var, threadvar, or inside a variant (case) part.
- A record containing a FAM cannot be embedded in another structured type.
- A record containing a FAM cannot be the element type of an array.
- A FAM-record cannot be a stand-alone variable, value parameter, or function result. Allocate via GetMem and use a pointer (PFamRec).
Reference parameters (
var,
const,
constref,
out) of FAM-record type stay legal; pointer-to-FAM-record (
PFamRec) is unrestricted.
Comparison with array of TA FAM is not a dynamic array. They share no run-time machinery:
- Storage: array of T - heap block referenced by a managed pointer; FAM - inline tail of the containing record.
- Lifetime: array of T - reference-counted, freed automatically; FAM - manual, freed with the containing block.
- SetLength: array of T - resizes; FAM - not applicable, size fixed by the original GetMem.
- Length(): array of T - current count; FAM - returns 0 (static length), runtime length is whatever you allocated.
- Range checking: array of T - runtime check; FAM - none.
- Overhead per record: array of T - one pointer + refcount; FAM - zero.
Want a managed, resizable array that lives elsewhere? Use
array of T. Want a fixed-shape tail that sits inline behind the record header in one block? Use a FAM.
Debugger viewA FAM has no statically known length, so without help the debugger sees an empty array. The compiler emits a DWARF expression for the upper bound that reads the count at runtime from a sibling ordinal field of the same record. fpdebug and gdb evaluate that expression on every variable refresh and pretty-print the FAM with the right element count.
By default, the compiler picks the
last ordinal field declared before the FAM as the count source. For the typical "header + count + payload" layout this needs no annotation:
type
PTokenPrivileges = ^TTokenPrivileges;
TTokenPrivileges = packed record
PrivilegeCount: DWORD;
Privileges: array[] of LUID_AND_ATTRIBUTES;
end;
In the Local Variables / Watches panel,
tp^.Privileges shows up with
PrivilegeCount elements expanded.
If the count is not the last ordinal field before the FAM, bind it explicitly with the optional
count clause:
type
TBatch = packed record
Count: DWORD;
Reserved: DWORD;
Items: array[] of TItem count Count;
end;
This is purely a debug-info detail - no runtime cost, no change to the record layout, no effect on
sizeof or indexing.
Use casesThe pattern shows up wherever a fixed header is followed by a variable-length tail in one block of memory:
- Win32 structures. BITMAPINFO, LOGPALETTE, TOKEN_GROUPS, TOKEN_PRIVILEGES, SOCKET_ADDRESS_LIST, SP_DRVINFO_DETAIL_DATA, and many more declare a trailing array as array[0..0] or ANYSIZE_ARRAY today, with all the problems above.
- Network protocol frames. TCP/UDP packets, WebSocket frames, MQTT messages, custom IPC payloads.
- File formats. BMP, WAV chunks, custom containers with a header and inline body.
- Inline buffers in records. Storing a payload at the tail of a node avoids a second allocation and improves cache locality.
DemoA real-world example using FAM with the Windows
TOKEN_PRIVILEGES structure to enumerate the privileges of the current process token:
program tokenprivilegesdemo;
{$mode unleashed}
uses SysUtils, Windows;
const
SE_PRIVILEGE_REMOVED = $00000004; // missing in Windows unit
type
// FAM record - PrivilegeCount auto-binds as the count for Privileges
TOKEN_PRIVILEGES = packed record
PrivilegeCount: DWORD;
Privileges: array[] of LUID_AND_ATTRIBUTES;
end;
procedure fatal(msg: string);
begin
writeln('FATAL: ', msg);
readln;
halt(1);
end;
function describePrivilege(la: LUID_AND_ATTRIBUTES): (name: WideString; attrs: string);
begin
var buf: array[0..255] of WideChar;
var len: dword := length(buf);
if not LookupPrivilegeNameW(nil, la.Luid, @buf[0], len) then buf[0] := #0;
result.name := WideCharToString(buf);
result.attrs := '';
match all
la.Attributes and SE_PRIVILEGE_ENABLED <> 0: result.attrs += 'ENABLED ';
la.Attributes and SE_PRIVILEGE_ENABLED_BY_DEFAULT <> 0: result.attrs += 'DEFAULT ';
la.Attributes and SE_PRIVILEGE_REMOVED <> 0: result.attrs += 'REMOVED ';
la.Attributes and SE_PRIVILEGE_USED_FOR_ACCESS <> 0: result.attrs += 'USED ';
end;
if result.attrs = '' then result.attrs := '-';
end;
function queryTokenSize(token: THANDLE): (ok: boolean; size: dword);
begin
result.size := 0;
GetTokenInformation(token, TokenPrivileges, nil, 0, result.size);
result.ok := GetLastError = ERROR_INSUFFICIENT_BUFFER;
end;
procedure main;
begin
// open access token of current process
var token: THANDLE;
if not OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, token) then fatal('OpenProcessToken failed');
defer CloseHandle(token);
// first call, get required buffer size
var (ok, size) := queryTokenSize(token);
if not ok then fatal('GetTokenInformation (size query) failed');
// second call, fetch actual data
var privs: ^TOKEN_PRIVILEGES;
GetMem(privs, size);
defer FreeMem(privs);
if not GetTokenInformation(token, TokenPrivileges, privs, size, size) then fatal('GetTokenInformation failed');
writeln('privilege count: ', privs^.PrivilegeCount);
for var i := 0 to privs^.PrivilegeCount - 1 do begin
var (name, attrs) := describePrivilege(privs^.Privileges[i]);
writeln(Format(' %-42s %s', [WideCharToString(@name[1]), attrs]));
end;
end;
begin
main;
readln;
end.
Output as a regular user:
privilege count: 6
SeLockMemoryPrivilege -
SeShutdownPrivilege -
SeChangeNotifyPrivilege ENABLED DEFAULT
SeUndockPrivilege -
SeIncreaseWorkingSetPrivilege -
SeTimeZonePrivilege -
Lazarus IDE: rebrandingThe Lazarus IDE fork that ships with Unleashed got a few cosmetic changes too: a new splash screen and "Unleashed" labels in a few places (about box, title bar, etc.) so it's clear at a glance which build you're running. Functionally identical, just visually distinct from stock Lazarus. Screenshots attached.