Free Pascal Dynamic Packages: Review Plugin Boundaries First

Evaluate Free Pascal dynamic packages with explicit toolchain pinning, ownership rules, unload tests and platform checks before shipping native plugins.

In this article

Dynamic packages can make a Pascal application extensible without rebuilding one giant executable. The difficult part is not loading a library; it is maintaining a safe boundary when classes, strings, callbacks, and threads cross between the host and a replaceable component.

As reviewed on October 19, 2026, the Get Lazarus dynamic-package announcement describes its own compiler, runtime, and IDE changes and links forked branches. Treat that as the scope of the announcement. It is not evidence that every standard Free Pascal installation, target architecture, or distribution package supports the same behavior.

Pin the complete build combination

Before designing the plugin interface, record the exact compiler build, runtime, target CPU, operating system, widgetset if applicable, and package source revision. Store that manifest with the host and every plugin artifact.

Do not reduce compatibility to “both are Pascal.” A library built with different runtime assumptions can appear to load while failing when a managed value is released. A matching filename or exported symbol is not enough evidence of compatible object layout or ownership.

Create a small compatibility gate that compares an explicit plugin interface version and expected host capabilities before constructing application objects. Report a clear mismatch rather than attempting to continue with a partly compatible component. Keep the error free of local secrets or sensitive installation paths.

Separate the interface from its implementation

Put shared declarations in a deliberately small contract package. Avoid exposing internal collections, global configuration objects, or application-wide singletons simply because native types make them convenient to reference.

For a hypothetical image-export plugin, the contract could describe an export request, a cancellation signal, and a result containing an output identifier. It need not grant direct access to every open document or every filesystem location.

Define what the host owns and what the plugin owns. For each object or buffer, answer who allocates it, who may retain it, who releases it, and what happens when cancellation occurs. Write these rules beside the interface, not only in an implementation comment that the other side never sees.

Treat unloading as a lifecycle operation

A library may still be in use after the visible task ends. A timer can hold a callback into its code. A worker can retain an object whose destructor lives in the package. A user-interface event can remain queued after its control disappears.

Design an explicit shutdown sequence:

  1. Stop accepting new plugin work.
  2. Ask active tasks to finish or cancel.
  3. Wait within a defined timeout and report unfinished work.
  4. Unregister callbacks, timers, and event handlers.
  5. Release plugin-owned instances through the agreed contract.
  6. Unload only when the host can demonstrate that no references remain.

If a plugin cannot reach that state, keeping it loaded until process exit may be safer than forcing an unload. A hot-replacement feature is not a reason to ignore live native references.

Test managed values and error paths

Build a tiny test plugin that returns a long string, an empty string, a dynamic array, and an object that fails partway through initialization. Repeat load, call, cleanup, and unload cycles under the diagnostic tools supported by your platform.

Count allocations and outstanding objects in your own contract layer. Deliberately cancel during a callback and verify that the callback is not invoked after shutdown. Test error reporting when the plugin throws or returns an unsuccessful result; the host must still clean up already-created resources.

Do not declare the interface safe because one successful call completed. Problems often appear when control flows backward through cleanup or when a second package shares a runtime type with the first.

Check package-specific restrictions

The announcement identifies restrictions around unit naming and package metadata, including the risk of removing required metadata during stripping. Review those rules against your release pipeline instead of applying ordinary shared-library packaging steps automatically.

For example, a generic artifact-minimization stage may strip all binaries after compilation. Exempting a package should be an explicit, reviewed build rule with a test that still loads and inspects the shipped artifact. Testing the unstripped development build would miss the release-only failure.

Also test naming collisions across the complete installed package set. Two independently useful plugins can still conflict if their units occupy the same naming space under the implementation’s rules.

Make platform support an observed fact

Maintain a matrix with a separate result for each OS, architecture, compiler build, and widgetset you actually ship. A documented intention to support a platform is not the same as a passing build and lifecycle test on that platform.

For an unsupported combination, fail installation clearly or retain a static build option. Do not silently fall back to a different binary from a nearby architecture. Native plugin failures can crash the whole host rather than produce a recoverable application error.

Conclusion

Dynamic packages offer a useful deployment boundary only when the lifecycle boundary is equally explicit. Pin the toolchain, minimize the contract, prove cleanup, and test the exact packaged binary on every supported target.

Advertisement