Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works

Random project

pascal-common-faa

Base library shared by the *-faa libraries for Delphi and Lazarus/FPC: optional/nullable types, portable atomics and monotonic time, monitor and thread pool.

View on GitHub

Anyone may press Interesting, signed in or not. It helps others find this project.

Member rating

No ratings yet.

Sign in to rate this project.

README

pascal-common-faa

Base library shared by the *-faa libraries — pascal-db-faa, pascal-named-pipes-faa, pascal-amqp-faa, pascal-redis-faa — for Delphi and Lazarus/Free Pascal (dual-compiler, FPC 3.2.2).

It holds what at least two of those libraries need, so each of them stops carrying its own copy. It also holds the optional types, which must be one shared type: a DTO filled from a message or a request can then be handed to the database layer as is. The library is small on purpose, and changes slowly.

Tested here on FPC 3.2.2 (Windows x64, Linux x86_64, Linux i386) and Delphi 12 (Win32, Win64). Delphi for Android (ARM) is a consumer target that has only been compiled so far, inside pascal-named-pipes-faa's Android test project, not run on a device. Nothing in the code is Windows-only, and the shared instances (PcPool, the default clock and sleep) are created in initialization instead of lazily, which is what keeps them safe on weakly ordered CPUs such as ARM.

Contents

Unit What it gives you
PascalCommon.Version PASCALCOMMON_VERSION (major × 10000 + minor × 100 + patch), for compile-time checks
PascalCommon.Threading Atomics (PcAtomicInc, Dec, Get, Set, CompareExchange; 64-bit PcAtomicInc64, Add64, Read64, Write64, CompareExchange64 for Int64 and UInt64) and monotonic time (PcTickMs, PcTickUs). TInterlocked and TStopwatch don't exist in FPC
PascalCommon.ThreadPool TPcMonitor (lock + condition variable), TPcThreadPool with TPcWorkItem, the process-wide pool PcPool, and PcProcessorCount (the real CPU count, also on FPC for Linux)
PascalCommon.SystemContext Replaceable wall clock (TClock), monotonic clock (TTicker) and wait (TSleep), so tests can run time-dependent code without real waits
PascalCommon.ClockCache TClockCache<K, V>: a thread-safe, fixed-size cache (G-Clock eviction) with predictable latency
PascalCommon.Cache (1.29.0) A cache with expiry and tag invalidation for an application's DTOs: IPcCache/TPcCache (Key with tags, TryGet, Put, GetOrLoad with single-flight, Invalidate by tag generations), a store that is down treated as a miss, metrics on PcMetrics; TPcMemoryCacheStore in process memory. The Redis store lives in pascal-redis-faa. See docs/cache-design.md
PascalCommon.SingleFlight (1.30.0) TPcSingleFlight<T>.Run(Key, Loader, Arg): the threads that ask for one key at the same time share one run of the loader (a token renewal, a tenant's first load, a reconnect); its exception reaches them all; nothing is kept after it. TPcCache.GetOrLoad runs on it. See docs/single-flight-design.md
PascalCommon.SafeLog SafeWriteln: a console Writeln safe to call from any thread, with one lock for the whole process, and a no-op in a binary without a console (service, GUI). PcSetSafeLogUtf8(True) makes it write UTF-8 to a file or a pipe and UTF-16 to a Windows console, the same on both compilers
PascalCommon.Utf8 PcTryUtf8BytesToString: UTF-8 bytes to a string that refuses bytes that aren't UTF-8 (UTF-16 included, 1.25.0) and, on FPC, to corrupt non-ASCII text when the process code page isn't UTF-8; PcIsValidUtf8; PcStringToUtf8Bytes, the other way; PcPercentEncode (1.26.0): RFC 3986 percent-encoding of the UTF-8 bytes ('João' → 'Jo%C3%A3o', a space %20), for a query value or a path segment
PascalCommon.Text PcUpperCase, PcLowerCase: upper and lower case that map every Unicode letter on both compilers, whatever the process locale (AnsiUpperCase on FPC/Linux depends on it; gotcha 10). PcCharCount (1.21.0): characters as code points, the same on both compilers. PcFoldForSearch (1.20.0): a search key, lower case with Latin accents removed and blanks collapsed ('São CONCEIÇÃO' → 'sao conceicao'), to store next to a name and compare with what the user types, the same on every database
PascalCommon.Crypto PcSha256, PcHmacSha256, PcBase64UrlEncode/Decode (and DecodeStrict, 1.24.0: only the encoder's text, for signatures), PcBytesToHex, PcSameBytes (constant time), PcPbkdf2HmacSha256 (password hashing, HMAC's states computed once); PcSha512, PcHmacSha512, PcPbkdf2HmacSha512 (1.28.0, SCRAM-SHA-512): FPC 3.2.2 has no SHA-256 or SHA-512; checked against FIPS 180, RFC 4231, RFC 4648 and RFC 7914
PascalCommon.AesGcm PcAesGcmEncrypt, PcAesGcmDecrypt: AES-128/192/256-GCM (authenticated encryption with additional data), pure Pascal, checked against NIST and OpenSSL vectors; for secrets at rest (the S-box is a table lookup: see the unit)
PascalCommon.Passwords (1.26.0) TPcPasswords.Hash, Verify, NeedsRehash: PBKDF2-HMAC-SHA256 with a random salt, stored as $pbkdf2-sha256$i=210000$<salt>$<hash> so the cost can grow later; Verify in constant time and never raising on what a database holds; ceilings on the password's length and a stored hash's iterations
PascalCommon.Random PcFillRandom, PcRandomBytes: random bytes from the operating system's cryptographic source (BCryptGenRandom, /dev/urandom), for secrets and ids; EPcRandom when it fails, never a fallback to Random
PascalCommon.TraceContext W3C Trace Context: new trace and span ids (PcNewTraceId, PcNewSpanId, from the OS random source), validation, and traceparent/tracestate parse and format (PcTryParseTraceParent, PcFormatTraceParent, PcResolveTraceState)
PascalCommon.Metrics Counter, up-down counter, gauge and histogram with labels, a registry (PcMetrics is the process-wide one), and the Prometheus text exposition (PcPrometheusText). OpenTelemetry names and units (http.server.request.duration, s) become Prometheus names (http_server_request_duration_seconds); recording is lock-free on a series
PascalCommon.Tracing Spans: TPcTracing.StartSpan (a child of the thread's current span), StartSpanWith, StartSpanFromParent (from a traceparent header), StartDetachedSpan (never the current span, finished on any thread) with StartChildSpan (an explicit parent), attributes and status; parent-based sampling with a ratio; a batch processor that hands finished spans to an IPcSpanExporter on its own thread. The exporter (OTLP over HTTP) lives in pascal-api-infra-faa
PascalCommon.StopSignal (1.26.0) A console program's stop request: Ctrl+C, Ctrl+Break and the console closing on Windows, SIGINT and SIGTERM (docker stop, systemd) on FPC/Unix, as a flag (PcStopRequested) and a sleep that ends on it (PcStopSleep). PcBlockStopSignals for a server's accepting thread on FPC/Unix (gotcha 14)
PascalCommon.WinHeap (1.26.0) Delphi on Windows: the program's memory on the Windows process heap, which scales with threads where the RTL's manager doesn't (pascal-db-faa gotcha 55). First unit of the program, or it does nothing; PcMemoryManagerName says which manager runs. Nothing on FPC
PascalCommon.Transport (1.27.0) The transport contract shared by the libraries: TPcEndpoint (one connection: Read, WriteExactly, CloseAbort from any thread, per-wait deadlines for both directions or each one, peer address and verified identity), TPcListener (with the Port bound, for port 0), TPcEndpointStream for frame readers, TPcTcpOptions/TPcTlsOptions (a zeroed record is the safe one), the EPcTransport* exceptions, PcParseHostPort. See docs/transport-design.md
PascalCommon.Transport.Tcp (1.27.0) PcTcpConnect (each resolved address once, IPv6 and IPv4, inside one budget; EPcTransportRefused, EPcTransportTimeout) and PcTcpListen: WinSock2 on Windows, BaseUnix sockets on FPC/Unix (PascalCommon.Transport.Posix); TCP_NODELAY and keep-alive
PascalCommon.Transport.Tls (1.27.0) PcTlsConnect, PcTlsListen, PcTlsListenOver, PcTlsWrap: TLS client and server with a private CA, mTLS and the client's verified identity, the hostname or IP check and a handshake deadline; the backend chosen at run time, SChannel (PascalCommon.Transport.SChannel, the default on Windows) or OpenSSL 3.x/1.1.1 (PascalCommon.Transport.OpenSSL, loaded on first use). A CA is PEM on both; one's own certificate is a PFX on SChannel and a PEM pair on OpenSSL
PascalCommon.Optionals Optional and nullable types: IOptXxx ("was it provided?"), INullXxx ("is it null?") and IOptNullXxx (both), for String, Integer, Int64, Single, Double, Currency, TDateTime, Boolean and TGUID, with TOptionals.Safe, string normalizers that keep the state (Trim, Upper, Lower, NullIfEmpty, UndefinedIfEmpty), and Map/Normalize with your own function or normalizer for every type, all in TOptionals and as chainable methods; ValueOr(default) on every one (1.19.0). TOptNullBoolean.From returns the interface IOptNullBoolean; for an IOptBoolean or INullBoolean use TrueValue/FalseValue
PascalCommon.Normalizers Value normalizers that don't depend on the optionals: a function type and a normalizer interface per type (TPcStringFunc/IPcStringNormalizer...), and one factory class per type (TPcStringNormalizers, TPcIntegerNormalizers...), to find them by completion and to inherit with your own: FromFunc, Chain, and Trim/Upper/Lower for string. The same normalizer works on a plain value (N.Apply(S)) and on an optional (X.Normalize(N))
PascalCommon.Validators (1.21.0) Checks that refuse a value and change nothing, the normalizers' partner: a validator interface and a factory per type (TPcStringValidators: Required, MinLength, MaxLength, OneOf; numbers and dates: Min, Max, Range, MultipleOf, MaxDecimals), FromFunc, All; TPcValidation collects every failing field (an optional checked only when it came) and raises them together as EPcValidationError; messages in English and Portuguese
PascalCommon.TimeZones (1.22.0) IANA time zones on both compilers (neither RTL converts an arbitrary zone): an instant's local time and offset, a local time's instant with the daylight-saving gap and overlap resolved (java.time's defaults, or your rule), a local day's bounds (23 h, 25 h...), zone names checked case-sensitively (US/Eastern and the other backward links accepted). The IANA tzdb is embedded (PascalCommon.TimeZones.Data, 2026e, about 200 KB), so the four targets give the same answers; a zoneinfo directory such as /usr/share/zoneinfo is an option. Instants are Unix seconds. (1.23.0) PcLocalIsoString(zone, instant): the zone's clock with its offset, 2027-10-31T01:00:00+01:00 then +00:00 an hour later in Lisbon; PcFormatUtcOffset
PascalCommon.JsonMapper.Optionals Optional bridge: a pascal-jsonmapper-faa converter for the optional types. Separate package
pascal
uses PascalCommon.Optionals, PascalCommon.Threading;

var
  LName: IOptString;
  LCount: Int64;
begin
  LName := TOptNullString.From('São Paulo');
  if TOptionals.Safe(LName).HasValue then
    WriteLn(LName.Value);
  LCount := 0;
  PcAtomicAdd64(LCount, 10);
end;

Normalizing an optional string without an if

TOptionals.Trim, Upper and Lower take an IOptString, INullString or IOptNullString and return the same flavor. Undefined stays Undefined and Null stays Null. Only a value that is present is changed. nil reads as it does in Safe, so the result is never nil. The result can go straight to a parameter, a message or a comparison:

pascal
// DTO of a PATCH: Name, State: IOptString; Nickname: IOptNullString
LQuery.Params.OptStrings['NAME']  := TOptionals.Trim(LDto.Name);
LQuery.Params.OptStrings['STATE'] := TOptionals.Upper(TOptionals.Trim(LDto.State));
LNickname := TOptionals.NullIfEmpty(TOptionals.Trim(LDto.Nickname)); // '   ' becomes Null
  • NullIfEmpty (INullString, IOptNullString) turns '' into Null.
  • UndefinedIfEmpty (IOptString, IOptNullString) turns '' into Undefined.
  • Neither one trims: compose with Trim to treat blanks as empty.
  • Upper/Lower map every Unicode letter ('são paulo' → 'SÃO PAULO') on both compilers whatever the process locale (on FPC through the Character unit; see docs/gotchas.md, gotcha 10).
  • Map(AValue, AFunc) applies a function of your own to a present value. AFunc is a TPcStringFunc = function(const AValue: string): string: a plain function, the same on both compilers, because FPC 3.2.2 has no closures.
pascal
function OnlyDigits(const AValue: string): string;
...
LZip := TOptionals.Map(LDto.Zip, OnlyDigits);

The same normalizers are methods of the string optionals (1.11.0), so they chain. Each one calls the TOptionals method of the same name, with the same rules:

pascal
LDto.State    := LDto.State.Trim.Upper;
LDto.Nickname := LDto.Nickname.Trim.NullIfEmpty;
LZip          := LDto.Zip.Map(OnlyDigits);

For a normalization with parameters, which a plain function can't carry, Normalize (1.12.0) is Map with an IPcStringNormalizer: a class with an Apply method.

pascal
type
  TMaxLength = class(TInterfacedObject, IPcStringNormalizer)
  private
    FMax: Integer;
  public
    constructor Create(AMax: Integer);
    function Apply(const AValue: string): string; // Result := Copy(AValue, 1, FMax)
  end;
...
LZip  := LDto.Zip.Trim.Map(OnlyDigits).Normalize(TMaxLength.Create(8));
LName := TOptionals.Normalize(LDto.Name, TMaxLength.Create(60));

The reference count frees the object, so there is no try/finally. One instance can also be kept in a variable and used for many fields.

Map and Normalize exist for every type, not only string (1.12.0). Each type has its own function type and normalizer interface: TPcIntegerFunc/IPcIntegerNormalizer, and likewise for Int64, Single, Double, Currency, DateTime, Boolean and Guid.

pascal
function RoundCents(const AValue: Currency): Currency;
...
LPrice    := LDto.Price.Map(RoundCents);
LDeadline := LDto.Deadline.Normalize(TAddBusinessDays.Create(3)); // your IPcDateTimeNormalizer

Single and Double keep the source's DecimalPlaces, except for a result of 0: From returns its shared zero, whose DecimalPlaces is -1.

One place for your normalizers

With dozens of normalizers, each its own class, finding them gets hard. The unit PascalCommon.Normalizers (1.14.0) has one factory class per type (TPcStringNormalizers, TPcIntegerNormalizers, ... TPcGuidNormalizers): type TPcStringNormalizers. and the editor lists them. Every factory has FromFunc (a plain function as a normalizer) and Chain (several applied in order, as one); the string one also has Trim, Upper and Lower. Your application inherits a factory and adds its own as class functions, so the editor lists yours and the library's under one name. The classes behind them stay in the implementation section:

pascal
unit App.Normalizers;

interface

uses PascalCommon.Normalizers; // no optionals needed

type
  TNorm = class(TPcStringNormalizers)
  public
    class function MaxLength(AMax: Integer): IPcStringNormalizer; static;
    class function OnlyDigits: IPcStringNormalizer; static;
    class function Cep: IPcStringNormalizer; static;
  end;

implementation

type
  TMaxLength = class(TInterfacedObject, IPcStringNormalizer) ... end;

function DigitsOf(const AValue: string): string; ...

class function TNorm.MaxLength(AMax: Integer): IPcStringNormalizer;
begin
  Result := TMaxLength.Create(AMax);
end;

class function TNorm.OnlyDigits: IPcStringNormalizer;
begin
  Result := FromFunc(DigitsOf);
end;

class function TNorm.Cep: IPcStringNormalizer;
begin
  Result := Chain([Trim, OnlyDigits, MaxLength(8)]); // a business rule, named once
end;

end.

The same normalizer serves an optional and a plain value:

pascal
LDto.Zip  := LDto.Zip.Normalize(TNorm.Cep);       // optional: the state is kept
LDto.Name := LDto.Name.Normalize(TNorm.Chain([TNorm.Trim, TNorm.MaxLength(60)]));

LZip := TNorm.Cep.Apply(EditZip.Text);             // plain string, no optionals

PascalCommon.Normalizers doesn't depend on PascalCommon.Optionals (the dependency goes Text → Normalizers → Optionals), so code that only normalizes plain values doesn't bring the optionals in. Keep the rule itself in a plain function and wrap it, as DigitsOf above: code that wants neither objects nor optionals calls the function directly.

Until 1.13 the interfaces and function types were declared in PascalCommon.Optionals, which still has them as aliases (the same types, not copies), so code that uses only that unit builds unchanged. The factories' 1.13 unit, PascalCommon.Optionals.Normalizers, was removed in 1.14: replace it with PascalCommon.Normalizers in the uses clause.

  • Trim, Upper and Lower return shared instances; they hold no state.
  • Chain([]) returns the value unchanged, and a nil item raises EArgumentException. The chain keeps a reference to each item, so Chain([TX.Create(...)]) frees TX with the chain.
  • The library adds a ready-made normalizer only when it is generic (case, blanks, length, digits, rounding, a date without its time) and a second library or a measured case needs it. Business rules (a CEP, a CPF, a phone number) belong in your factory.
  • Use inheritance, not a class helper: Delphi and FPC 3.2 see one helper per type, so two units with helpers for the same class hide each other. A library on top of this one can have its own factory that inherits this one, and the application can inherit that library's factory.

A method can't be called on nil. Where the field may be nil, start the chain with Safe (TOptionals.Safe(LDto.State).Trim.Upper), or use the TOptionals form, which reads nil as Safe does. IOptString, INullString and IOptNullString are implemented only by TOptNullString. Don't implement them in your own classes: a minor version may add methods to them.

Only the string optionals have normalizers. The other types get theirs when a consumer needs them.

Time zones

pascal
uses PascalCommon.TimeZones;

var
  LZone: IPcTimeZone;
  LSpelled: string;
  LStart, LDayStart, LDayEnd: Int64;
begin
  if not PcTimeZones.TryFind(ATenantZone, LZone) then
    if PcTimeZones.TryCanonicalCase(ATenantZone, LSpelled) then
      raise Exception.CreateFmt('Unknown time zone; did you mean %s?', [LSpelled])
    else
      raise Exception.Create('Unknown time zone');
  // Monday's 09:00 in the tenant's zone, as Unix seconds.
  LStart := LZone.ToUtc(TPcLocalDateTime.Create(2026, 3, 9, 9, 0));
  // The local day 2026-03-08 in New York lasts 23 hours.
  LZone.DayBounds(2026, 3, 8, LDayStart, LDayEnd);
  WriteLn(LZone.ToLocal(LStart).IsoWeekday);    // 1 = Monday
end;

A local time that doesn't exist (02:30 when the clock jumps from 02:00 to 03:00) moves forward by the gap, to 03:30; one that happens twice (01:30 when the clock goes back) takes the first. Pass gapStartOfAfter, gapRaise, overlapLater or overlapRaise to ToUtc for another rule, or call Map to see every answer. Design, measurements and the update policy for the embedded data: docs/timezones-design.md.

A cache in front of a slow query

pascal
uses PascalCommon.Cache;

// Composition root: one cache for the process.
FCache := TPcCache.Create(TPcMemoryCacheStore.Create(10000), 'app');

function TProductService.ListByCategory(ACategoryId: Integer): TProductList;
var
  LKey, LJson: string;
begin
  LKey := FCache.Key('product.list:' + IntToStr(ACategoryId),
    ['products', 'category:' + IntToStr(ACategoryId)]);
  if FCache.TryGet(LKey, LJson) then
    Exit(JsonToProducts(LJson));                       // hit: no database
  Result := FRepository.ListByCategory(ACategoryId);
  FCache.Put(LKey, ProductsToJson(Result), 300);       // 5 minutes
end;

procedure TProductService.UpdatePrice(AProductId, ACategoryId: Integer; APrice: Currency);
begin
  FRepository.UpdatePrice(AProductId, APrice);         // commits inside
  FCache.Invalidate(['category:' + IntToStr(ACategoryId)]);  // after the commit
end;

GetOrLoad(Key, Ttl, Loader, Arg) does the same with a method as the loader, and runs it once when many threads miss the same key together. Invalidate after the commit, never before, and don't cache what a transaction that also wrote has read: it may be rolled back. A store that is down (Redis, through pascal-redis-faa's store) is a miss, not an error; the TTL bounds how long a value can be stale when an invalidation fails. Values are strings (your DTO's JSON), and keys show in Redis tools: no secrets in them.

One run of a slow call for every thread that asks at once

pascal
uses PascalCommon.SingleFlight;

// A field of the client, created with it: FRenew := TPcSingleFlight<string>.Create;
function TApiClient.RenewToken(const AArg: string): string;   // the loader: a method
begin
  if not TokenExpired then          // a flight that just ended may have renewed it
    Exit(CurrentToken);
  Result := FetchNewToken;          // its own HTTP timeout bounds everybody's wait
  StoreToken(Result);
end;

function TApiClient.Token: string;
begin
  if not TokenExpired then
    Exit(CurrentToken);
  Result := FRenew.Run('token', RenewToken, '');
end;

The first thread runs the loader; the others that ask for 'token' while it runs wait and get its result. If it raises, each of them gets an exception of the same class and message (EPcSingleFlightError when the class can't be built again, such as EOutOfMemory). It is not a cache: the next call after the flight runs the loader again, so check first, as above. Only the first caller's loader and argument run, so one key must mean one piece of work. T is a value, a string, a record or an interface, never a class. One process only; across instances, pascal-redis-faa's TRedisLock. Design: docs/single-flight-design.md.

Stopping a server cleanly

pascal
program MyServer;

uses
  {$IFDEF UNIX}cthreads,{$ENDIF}
  PascalCommon.WinHeap,  // Delphi/Windows: first unit, before anything allocates
  SysUtils, Classes, PascalCommon.StopSignal;

type
  TStopWatcher = class(TThread)
  protected
    procedure Execute; override;
  end;

procedure TStopWatcher.Execute;
begin
  while not Terminated and not PcStopRequested do
    PcStopSleep(200);
  if not Terminated then
    StopTheServer;  // makes Listen below return
end;

var
  LWatcher: TStopWatcher;
begin
  WriteLn('memory: ', PcMemoryManagerName);
  PcStopSignalInstall;
  LWatcher := TStopWatcher.Create(False);
  PcBlockStopSignals;  // after the watcher: it must still receive SIGTERM
  try
    Listen;            // blocks in accept until StopTheServer
  finally
    LWatcher.Terminate;
    LWatcher.WaitFor;
    LWatcher.Free;
    PcStopSignalDone;
    PcStopSignalUninstall;
  end;
end.

A worker without a server needs only the loop: while not PcStopRequested do begin DoOnePass; PcStopSleep(5000); end;.

Installing

Lazarus: open and install packages/pascal_common_faa.lpk. For the JSON bridge, also packages/pascal_common_faa_jsonmapper.lpk, which requires pascal-jsonmapper-faa's pascaljsonmapper_pkg by name: install or require that one first. A project should name the copy it wants with Prefer="True" (see docs/gotchas.md, gotcha 2).

Delphi: add src to the project's search path (and bridges/jsonmapper plus pascal-jsonmapper-faa's src for the bridge).

FPC on Unix: PascalCommon.ThreadPool starts threads, so the program needs cthreads as the first unit in its uses.

For library authors: one copy per application

An application that uses several *-faa libraries gets one copy of this one, which it provides itself: one registered pascal_common_faa.lpk in Lazarus, or one search path in Delphi. That is why:

  • A library never ships pascal-common-faa inside itself. It may keep it as a submodule in external/ for its own tests and CI, but not in what the application builds. An application that clones a library with --recursive gets that external/ copy on disk too, and must never put it on its search path.
  • Versions follow strict semver. From 1.0 on, a minor version only adds, so the newest copy serves every library.
  • Each library checks the minimum version it needs, so an application with an older copy gets a clear build error instead of a missing identifier:
pascal
uses PascalCommon.Version;

{$IF PASCALCOMMON_VERSION < 10400}
  {$MESSAGE FATAL 'my-library needs pascal-common-faa 1.4 or later'}
{$IFEND}

Moving a library onto this one: docs/migrating.md has the name map.

Documentation

Tests

DUnitX on Delphi and FPCUnit on FPC, with the same test bodies: the FPCUnit fixtures are generated from the DUnitX ones by tools/gen_fpc_mirror.py. Acceptance is every test green and 0 leaks.

  • FPC on Windows: sh tools/test_fpc.sh
  • FPC on Linux, x86_64 and i386, in Docker (what CI runs): sh tools/ci-test.sh (it also sends a real SIGTERM to tests/Signals' probe: tools/test_stop_signal_docker.sh)
  • Delphi: open PascalCommon.groupproj, build and run tests/Unit/PascalCommon.UnitTests.dproj (Win32 and Win64)

The bridge tests need the submodule: git submodule update --init.

License

MIT — see LICENSE.