JavaScript Integration for Dynamic Applications
mORMot provides JavaScript scripting capabilities for applications that need dynamic behavior, user-defined workflows, or hot-reloadable business logic. This chapter covers the scripting architecture and available engines.
| Scenario | Benefit |
|---|---|
| User-defined workflows | End-users customize behavior without recompilation |
| Business rules | Domain experts define evolving logic |
| Customization | Single executable serves multiple clients |
| Reporting | Dynamic report definitions |
| Plugin systems | Third-party extensions |
| Rapid prototyping | Quick iteration without compilation |
mORMot chose JavaScript for scripting because:
- Universal knowledge - Nearly every programmer knows JavaScript
- Powerful when used well - Modern ES2020+ features
- Rich ecosystem - Thousands of libraries available
- Client/server sharing - Same validation logic on both sides
- Proven engines - QuickJS and SpiderMonkey are production-ready
┌─────────────────────────────────────────────────────────────────┐
│ Application Code │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ mormot.script.core.pas │
│ (Abstract Engine Management Layer) │
│ │
│ ┌─────────────────────┐ ┌──────────────────────────────┐ │
│ │ TThreadSafeManager │ │ TThreadSafeEngine (abstract) │ │
│ │ │ │ │ │
│ │ • Engine pooling │ │ • Per-thread execution │ │
│ │ • Thread safety │ │ • Context isolation │ │
│ │ • Hot reload │ │ • FPU state protection │ │
│ │ • Remote debugging │ │ • Lifecycle hooks │ │
│ └─────────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────────┐
│ Engine Implementations │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ mormot.lib.quickjs │ │ (SpiderMonkey planned) │ │
│ │ │ │ │ │
│ │ • ES2020 support │ │ • JIT compilation │ │
│ │ • Small footprint │ │ • High performance │ │
│ │ • Static linking │ │ • node.js compatible │ │
│ └──────────────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Engine | Use Case | Status |
|---|---|---|
| QuickJS | Client-side, embedded | Available (via mormot.lib.quickjs) |
| SpiderMonkey | Server-side, high-performance | Planned |
QuickJS is a small, embeddable JavaScript engine by Fabrice Bellard:
- ES2020 support - Modules, async/await, BigInt, Proxies
- Small footprint - ~400KB static library
- No external dependencies - Fully standalone
- Cross-platform - Windows, Linux, macOS
The mormot.lib.quickjs.pas unit provides direct QuickJS bindings. Here's a complete working example demonstrating script execution, function calls, and proper memory management:
uses
mormot.lib.quickjs,
mormot.core.base;
procedure QuickJSExample;
var
Runtime: JSRuntime;
Context: JSContext;
Script, FuncName: RawUtf8;
Global, Func, Res: JSValue;
Args: array[0..1] of JSValue;
ResultInt: integer;
begin
// 1. Create runtime and context
Runtime := JS_NewRuntime;
if Runtime = nil then
raise Exception.Create('Failed to create JSRuntime');
Context := JS_NewContext(Runtime);
if Context = nil then
begin
JS_FreeRuntime(Runtime);
raise Exception.Create('Failed to create JSContext');
end;
try
// 2. Execute JavaScript to define a function
Script := 'function multiply(a, b) { return a * b; }';
Res.Raw := JS_Eval(Context, pointer(Script), length(Script),
'init.js', JS_EVAL_TYPE_GLOBAL);
// Check for script errors (IsException and type checks are on JSValue)
if Res.IsException then
begin
WriteLn('Script error');
Context^.Free(Res);
Exit;
end;
Context^.Free(Res); // Free eval result
// 3. Get the global object (JS_GetGlobalObject returns JSValueRaw)
Global.Raw := JS_GetGlobalObject(Context);
// 4. Get the function by name (JS_GetPropertyStr returns JSValueRaw)
FuncName := 'multiply';
Func.Raw := JS_GetPropertyStr(Context, Global.Raw, pointer(FuncName));
if JS_IsFunction(Context, Func.Raw) = 0 then
begin
WriteLn('multiply is not a function');
Context^.Free(Func);
Context^.Free(Global);
Exit;
end;
// 5. Prepare arguments: integer tags need no heap allocation
Args[0].From32(7);
Args[1].From32(6);
// 6. Call the function (JS_Call returns JSValueRaw)
Res.Raw := JS_Call(Context, Func.Raw, Global.Raw, 2, @Args[0]);
// 7. Get result (IsNumber, IsException are methods on JSValue)
if Res.IsNumber then
begin
if JS_ToInt32(Context, @ResultInt, Res.Raw) = 0 then
WriteLn('7 × 6 = ', ResultInt);
end
else if Res.IsException then
WriteLn('Function call error');
// 8. Clean up JSValues that reference heap objects
Context^.Free(Res);
// Args[0] and Args[1] are JS_TAG_INT — no heap allocation, no Free needed
Context^.Free(Func);
Context^.Free(Global);
finally
// 9. Free context and runtime
JS_FreeContext(Context);
JS_FreeRuntime(Runtime);
end;
end;Key Points:
- Memory Management: Every
JSValuereferencing a heap object (strings, objects) must be freed viaContext^.Free(v). There is no standaloneJS_FreeValueprocedure; use theTJSContext.Freemethod instead. - Raw vs. Wrapper: Most QuickJS C-API functions (
JS_Eval,JS_Call,JS_GetPropertyStr, etc.) return/acceptJSValueRaw. Assign toJSValuevia the.Rawproperty. Type-checking helpers (IsException,IsNumber,IsString,IsObject) are methods onJSValue, not onJSValueRaw. - No JS_NewInt32: There is no
JS_NewInt32()helper. Create integer-tagged values usingJSValue.From32(val)instead. - Checking Functions: Use
JS_IsFunction(ctx, val.Raw)(returns 0/1) to test whether a value is callable;JSValuehas noIsFunctionmethod. - Cleanup Order: Free values before freeing context, and context before runtime.
QuickJS requires the LIBQUICKJSSTATIC conditional:
// In project options or .inc file:
{$define LIBQUICKJSSTATIC}Static object files are located under the static/ folder, in platform-specific subdirectories:
static/
x86_64-linux/quickjs.o // Linux 64-bit (FPC)
i386-linux/quickjs.o // Linux 32-bit (FPC)
aarch64-linux/quickjs.o // Linux ARM64 (FPC)
arm-linux/quickjs.o // Linux ARM (FPC)
x86_64-win64/quickjs.o // Windows 64-bit (FPC)
i386-win32/quickjs.o // Windows 32-bit (FPC)
delphi/quickjs.obj // Windows 32-bit (Delphi)
delphi/quickjs.o // Windows 64-bit (Delphi)
The TThreadSafeManager class provides thread-safe engine pooling:
Note:
TThreadSafeEngineis an abstract base class. You must implement your own concrete engine class (e.g.,TMyQuickJSEngine) that inherits from it and provides the actual scripting functionality.
uses
mormot.script.core;
type
// Your engine implementation (inherits from TThreadSafeEngine)
// NOTE: This is user-implemented - not provided by mORMot
TMyQuickJSEngine = class(TThreadSafeEngine)
protected
procedure AfterCreate; override;
procedure DoBeginRequest; override;
procedure DoEndRequest; override;
end;
var
Manager: TThreadSafeManager;
begin
Manager := TThreadSafeManager.Create(TMyQuickJSEngine);
try
// Configure expiration (recommended for long-running servers)
Manager.EngineExpireTimeOutMinutes := 240; // 4 hours
// Use in request handlers...
finally
Manager.Free;
end;
end;procedure HandleRequest;
var
Engine: TThreadSafeEngine;
begin
// Get or create engine for current thread
Engine := Manager.ThreadSafeEngine;
// Execute within protected context
Engine.ThreadSafeCall(
procedure(E: TThreadSafeEngine)
begin
// FPU state is protected here
// Execute JavaScript safely
end);
end;┌─────────────────────────────────────────────────────────────────┐
│ Thread Safety Model │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Thread 1 ──────► Engine 1 (exclusive) │
│ │
│ Thread 2 ──────► Engine 2 (exclusive) │
│ │
│ Thread 3 ──────► Engine 3 (exclusive) │
│ │
│ ▲ │
│ │ │
│ ┌────────────────┴────────────────┐ │
│ │ TThreadSafeManager │ │
│ │ (thread-safe pool management) │ │
│ └─────────────────────────────────┘ │
│ │
│ Key Rules: │
│ • One engine per thread (never shared) │
│ • Manager handles allocation/deallocation │
│ • Automatic expiration prevents memory leaks │
│ │
└─────────────────────────────────────────────────────────────────┘
// Application detects script changes
if ScriptFilesModified then
begin
// Increment version - engines will recreate on next access
Manager.ContentVersion := Manager.ContentVersion + 1;
end;
// In request handler
Engine := Manager.ThreadSafeEngine;
if Engine.ContentVersion <> Manager.ContentVersion then
begin
// Engine was recreated - reload scripts
LoadScriptsIntoEngine(Engine);
Engine.ContentVersion := Manager.ContentVersion;
end;For long-running servers, prevent JavaScript memory leaks:
// Recreate engines every 4 hours
Manager.EngineExpireTimeOutMinutes := 240;
// Mark critical engines as permanent
MainEngine.NeverExpire := true;mORMot implements the Firefox Remote Debugging Protocol:
// Start debugger on port 6000
Manager.StartDebugger('6000');
// Optional: Break on first line
Manager.PauseDebuggerOnFirstStep := true;- Open Firefox
- Navigate to
about:debugging - Click "This Firefox" → "Settings"
- Enable Remote Debugging
- Connect to
localhost:6000
Note: This uses Firefox DevTools protocol, NOT Chrome DevTools.
⚠️ NOTE: The high-level APIs shown in sections 22.7-22.8 are illustrative patterns, not exact API signatures. Themormot.script.quickjsunit providesTQuickJSEnginewithEvaluate(),RegisterMethod(),Globalvariant andTQuickJSVariantfor late-binding. The specific method names in the examples below (e.g.RegisterFunction,Engine.Call) differ from the actual API names.For exact signatures, see
mormot.script.quickjs.pas. For maximum control use the low-level QuickJS C API inmormot.lib.quickjs.pas(see Section 22.3).
// Register native function to access ORM
procedure RegisterOrmAccess(Engine: TThreadSafeEngine);
begin
// Example: Expose Customer retrieval
Engine.RegisterFunction('getCustomer',
function(Args: TJSArgs): TJSValue
var
ID: TID;
Customer: TOrmCustomer;
begin
ID := Args[0].AsInt64;
Customer := TOrmCustomer.Create;
try
if Server.Orm.Retrieve(ID, Customer) then
Result := CustomerToJS(Customer)
else
Result := JS_NULL;
finally
Customer.Free;
end;
end);
end;// JavaScript can call interface-based services
Engine.RegisterFunction('callService',
function(Args: TJSArgs): TJSValue
var
ServiceName, MethodName: RawUtf8;
Params: TDocVariantData;
begin
ServiceName := Args[0].AsString;
MethodName := Args[1].AsString;
Params := Args[2].AsVariant;
// Execute service and return result as JSON
Result := ExecuteServiceMethod(ServiceName, MethodName, Params);
end);// Define validation rules in JavaScript
const
VALIDATION_SCRIPT = '''
function validateOrder(order) {
if (order.total > 10000 && !order.managerApproval) {
return { valid: false, error: "Manager approval required" };
}
if (order.items.length === 0) {
return { valid: false, error: "Order must have items" };
}
return { valid: true };
}
''';
// Use from Delphi
function ValidateOrder(const Order: TOrder): boolean;
var
Engine: TThreadSafeEngine;
Result: Variant;
begin
Engine := Manager.ThreadSafeEngine;
Result := Engine.Call('validateOrder', [OrderToVariant(Order)]);
ValidateOrder := Result.valid;
if not ValidateOrder then
raise EValidation.Create(Result.error);
end;mORMot provides a custom variant type for seamless JavaScript access:
var
JSObj: Variant;
begin
// Create JavaScript object
JSObj := Engine.NewObject;
// Late-binding property access (like JavaScript)
JSObj.name := 'John';
JSObj.age := 30;
JSObj.greet := Engine.Function('return "Hello, " + this.name');
// Call method
Writeln(JSObj.greet()); // "Hello, John"
end;var
JSArray: Variant;
begin
JSArray := Engine.NewArray;
// Push elements
JSArray.push(1);
JSArray.push(2);
JSArray.push(3);
// Access by index
Writeln(JSArray[0]); // 1
// Array methods
JSArray.sort();
JSArray.reverse();
end;| Unit | Purpose |
|---|---|
mormot.script.core |
Abstract engine management, thread pooling |
mormot.script.quickjs |
QuickJS high-level wrapper (TQuickJSEngine, TQuickJSVariant) |
mormot.lib.quickjs |
Low-level QuickJS API bindings |
mormot.lib.static |
Static library loading infrastructure |
- ✓ Abstract
TThreadSafeEngineandTThreadSafeManager - ✓ Thread-safe pooling with expiration
- ✓ Remote debugging infrastructure
- ✓ Low-level QuickJS bindings (
mormot.lib.quickjs) - ✓
TQuickJSEnginehigh-level wrapper (mormot.script.quickjs) - ✓
TQuickJSVariantcustom variant type for late-binding JavaScript access
- ORM/SOA integration helpers
- SpiderMonkey integration (for high-performance server scenarios)
- JIT compilation support
- node.js module compatibility
| Use Scripting For | Use Compiled Code For |
|---|---|
| User-customizable logic | Performance-critical paths |
| Frequently changing rules | Core business logic |
| Plugin/extension systems | Security-sensitive operations |
| Rapid prototyping | Database access layer |
| Report templates | Infrastructure code |
⚠️ NOTE: The security control properties shown below (DisableFileAccess,MaxExecutionTimeMs, etc.) are conceptual names not present in the current API.Use
TQuickJSEngine.TimeoutValue(in seconds) for execution timeout control. QuickJS C API functionsJS_SetMaxStackSize()andJS_SetMemoryLimit()are available via the low-level bindings for stack and memory limits.
// Conceptual API - limit script capabilities
Engine.DisableFileAccess := true;
Engine.DisableNetworkAccess := true;
Engine.MaxExecutionTimeMs := 5000; // 5 second timeout
Engine.MaxMemoryBytes := 64 * 1024 * 1024; // 64MB limittry
Result := Engine.Eval(Script);
except
on E: EJSException do
begin
Log.Error('JavaScript error at line %d: %s',
[E.LineNumber, E.Message]);
// E.StackTrace contains full JS stack
end;
end;| Need | Solution |
|---|---|
| Embed JavaScript | mormot.lib.quickjs + LIBQUICKJSSTATIC |
| Thread-safe execution | TThreadSafeManager |
| Engine pooling | Manager.ThreadSafeEngine |
| Hot reload | Manager.ContentVersion |
| Remote debugging | Manager.StartDebugger('6000') |
| Memory management | EngineExpireTimeOutMinutes |
| QuickJS | SpiderMonkey (when available) |
|---|---|
| Client-side scripts | Server-side heavy processing |
| Small footprint needed | JIT performance required |
| Static linking preferred | node.js compatibility needed |
| ES2020 sufficient | Latest ECMAScript features |
Note: The scripting layer in mORMot2 is actively evolving. Check the source code and forum for the latest implementation status.
Next: Chapter 23 covers Asymmetric Encryption (ECC) for secure communications.
| Previous | Index | Next |
|---|---|---|
| Chapter 21: Security | Index | Chapter 23: Asymmetric Encryption |