# 17. Cross-Platform Clients
Reaching Beyond Windows
mORMot 2 is designed from the ground up as a cross-platform framework. The core libraries compile natively on Windows, Linux, macOS, FreeBSD, and Android using either Delphi or Free Pascal. This chapter covers strategies for consuming mORMot services from various platforms and generating client code.
Unlike mORMot 1 (which required separate SynCrossPlatform units), mORMot 2's main units are inherently cross-platform:
| Platform | Delphi | Free Pascal |
|---|---|---|
| Windows (32/64-bit) | ✅ | ✅ |
| Linux (x86_64, aarch64) | ✅ (Delphi 12+) | ✅ |
| macOS (x86_64, aarch64) | ✅ | ✅ |
| FreeBSD | — | ✅ |
| Android | ✅ (FireMonkey) | ✅ |
| iOS | ✅ (FireMonkey) | — |
The mormot.core. units provide the foundation:
| Unit | Purpose |
|---|---|
mormot.core.base |
Base types, memory management |
mormot.core.os |
OS abstraction (files, threads, processes) |
mormot.core.unicode |
UTF-8/UTF-16 handling |
mormot.core.text |
Text processing, formatting |
mormot.core.buffers |
Binary data handling |
mormot.core.data |
Collections, dynamic arrays |
mormot.core.json |
JSON parsing and generation |
mormot.core.variants |
TDocVariant for flexible JSON |
mormot.core.rtti |
Cross-platform RTTI |
mormot.core.interfaces |
Interface invocation, stubs, mocks |
HTTP client support via mormot.net.*:
uses
mormot.net.client,
mormot.rest.http.client;
var
Client: TRestHttpClientSocket;
begin
// Works on Windows, Linux, macOS, etc.
Client := TRestHttpClientSocket.Create('api.example.com', '443', Model, True);
try
Client.ServiceDefine([IMyService], sicShared);
// Use services...
finally
Client.Free;
end;
end;
Available HTTP client classes:
| Class | Transport | Platform |
|---|---|---|
TRestHttpClientSocket |
Raw sockets | All |
TRestHttpClientWebSockets |
WebSocket | All |
TRestHttpClientWinHttp |
WinHTTP API | Windows |
TRestHttpClientCurl |
libcurl | Linux/macOS |
mORMot 2 includes a powerful code generation system in mormot.soa.codegen.pas that creates client wrappers from server definitions using Mustache templates.
Key functions:
| Function | Purpose |
|---|---|
ContextFromModel() |
Extract ORM/SOA metadata as JSON |
WrapperFromModel() |
Generate code from Mustache template |
WrapperMethod() |
HTTP handler for browser-based generation |
AddToServerWrapperMethod() |
Add wrapper endpoint to server |
program MyServer;
uses
mormot.rest.server,
mormot.rest.http.server,
mormot.soa.codegen;
var
Server: TRestServerDB;
HttpServer: TRestHttpServer;
begin
Server := TRestServerDB.Create(Model, 'data.db3');
try
Server.ServiceDefine(TMyService, [IMyService], sicShared);
// Add wrapper generation endpoint
AddToServerWrapperMethod(Server, ['./templates', '../templates']);
HttpServer := TRestHttpServer.Create('8080', [Server]);
try
WriteLn('Wrapper available at http://localhost:8080/root/wrapper');
ReadLn;
finally
HttpServer.Free;
end;
finally
Server.Free;
end;
end.
Navigate to http://localhost:8080/root/wrapper in your browser:
Client Wrappers
===============
Available Templates:
- Delphi
mORMotClient.pas - download as file | see as text | see template
- TypeScript
mORMotClient.ts - download as file | see as text | see template
- OpenAPI
openapi.json - download as file | see as text | see template
Template context (JSON)
Templates use the Mustache logic-less syntax:
// Generated client for {{root}} API
unit {{filename}};
interface
uses
mormot.core.base,
mormot.rest.client;
type
{{#services}}
{{interfaceName}} = interface(IInvokable)
['{{{guid}}}']
{{#methods}}
{{declaration}};
{{/methods}}
end;
{{/services}}
implementation
{{#services}}
// {{interfaceName}} implementation
{{#methods}}
function T{{serviceName}}.{{methodName}}({{args}}): {{resultType}};
begin
// Generated stub code
end;
{{/methods}}
{{/services}}
end.
The context includes:
| Variable | Description |
|---|---|
{{root}} |
API root URI |
{{port}} |
Server port |
{{filename}} |
Output filename |
{{orm}} |
Array of ORM classes |
{{services}} |
Array of service interfaces |
{{#service.methods}} |
Methods within service |
{{typeDelphi}} |
Delphi type name |
{{typeTS}} |
TypeScript type name |
{{typeCS}} |
C# type name |
{{typeJava}} |
Java type name |
The simplest approach — use mORMot directly on any supported platform:
program CrossPlatformClient;
{$APPTYPE CONSOLE}
uses
mormot.core.base,
mormot.core.os,
mormot.orm.core,
mormot.rest.http.client,
MyServiceInterface;
var
Client: TRestHttpClientSocket;
Service: IMyService;
begin
Client := TRestHttpClientSocket.Create('server.example.com', '8080',
TOrmModel.Create([], 'api'));
try
Client.ServiceDefine([IMyService], sicShared);
if Client.Services.Resolve(IMyService, Service) then
WriteLn('Result: ', Service.Calculate(10, 20));
finally
Client.Free;
end;
end.
This compiles and runs identically on Windows, Linux, and macOS.
For projects that can't include full mORMot dependencies, use generated wrappers:
// Generated mORMotClient.pas
unit mORMotClient;
interface
uses
mormot.core.base,
mormot.rest.client;
type
ICalculator = interface(IInvokable)
['{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}']
function Add(n1, n2: Integer): Integer;
function Multiply(n1, n2: Int64): Int64;
end;
/// Create a connected client instance
function GetClient(const aServer: RawUtf8;
const aPort: RawUtf8 = '8080'): TRestHttpClientSocket;
implementation
function GetClient(const aServer, aPort: RawUtf8): TRestHttpClientSocket;
begin
Result := TRestHttpClientSocket.Create(aServer, aPort,
TOrmModel.Create([], 'api'));
Result.ServiceDefine([ICalculator], sicShared);
end;
end.
For Free Pascal, additional RTTI registration may be needed:
procedure ComputeFPCInterfacesUnit(const Path: array of TFileName;
DestFileName: TFileName = '');
This generates a unit with explicit interface registration to work around FPC RTTI limitations.
mORMot services use standard HTTP with JSON:
Request Format:
GET /api/Calculator/Add?n1=10&n2=20 HTTP/1.1
Host: server.example.com
Response Format:
{"result": 30}
For POST requests with complex parameters:
POST /api/Calculator.Add HTTP/1.1
Content-Type: application/json
[10, 20]
mORMot's default authentication uses a challenge-response protocol:
1. Client requests timestamp: GET /api/auth
2. Server returns: {"result": "1234567890"}
3. Client computes: HMAC-SHA256(password, timestamp + username)
4. Client authenticates: GET /api/auth?UserName=xxx&PasswordHashHexa=yyy&ClientNonce=zzz
5. Server returns session info
For simpler integration, consider:
Authorization: Basic base64(user:pass)Authorization: Bearer jwt_tokenServer.AuthenticationRegister(TRestServerAuthenticationNone)Example generated TypeScript client:
// mORMotClient.ts
export interface ICalculator {
add(n1: number, n2: number): Promise<number>;
multiply(n1: number, n2: number): Promise<number>;
}
export class CalculatorClient implements ICalculator {
constructor(private baseUrl: string) {}
async add(n1: number, n2: number): Promise<number> {
const response = await fetch(
`${this.baseUrl}/Calculator/Add?n1=${n1}&n2=${n2}`
);
const data = await response.json();
return data.result;
}
async multiply(n1: number, n2: number): Promise<number> {
const response = await fetch(
`${this.baseUrl}/Calculator/Multiply?n1=${n1}&n2=${n2}`
);
const data = await response.json();
return data.result;
}
}
// Usage
const calc = new CalculatorClient('http://localhost:8080/api');
const sum = await calc.add(10, 20);
import requests
class CalculatorClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/')
def add(self, n1: int, n2: int) -> int:
response = requests.get(
f"{self.base_url}/Calculator/Add",
params={"n1": n1, "n2": n2}
)
return response.json()["result"]
def multiply(self, n1: int, n2: int) -> int:
response = requests.get(
f"{self.base_url}/Calculator/Multiply",
params={"n1": n1, "n2": n2}
)
return response.json()["result"]
# Usage
calc = CalculatorClient("http://localhost:8080/api")
print(calc.add(10, 20)) # 30
using System.Net.Http.Json;
public interface ICalculator
{
Task<int> Add(int n1, int n2);
Task<long> Multiply(long n1, long n2);
}
public class CalculatorClient : ICalculator
{
private readonly HttpClient _client;
private readonly string _baseUrl;
public CalculatorClient(string baseUrl)
{
_client = new HttpClient();
_baseUrl = baseUrl.TrimEnd('/');
}
public async Task<int> Add(int n1, int n2)
{
var response = await _client.GetFromJsonAsync<ResultWrapper<int>>(
$"{_baseUrl}/Calculator/Add?n1={n1}&n2={n2}");
return response.Result;
}
public async Task<long> Multiply(long n1, long n2)
{
var response = await _client.GetFromJsonAsync<ResultWrapper<long>>(
$"{_baseUrl}/Calculator/Multiply?n1={n1}&n2={n2}");
return response.Result;
}
private record ResultWrapper<T>(T Result);
}
Create a Mustache template for OpenAPI 3.0:
{
"openapi": "3.0.0",
"info": {
"title": "{{root}} API",
"version": "1.0.0"
},
"servers": [
{"url": "http://localhost:{{port}}/{{root}}"}
],
"paths": {
{{#services}}
{{#methods}}
"/{{../serviceName}}/{{methodName}}": {
"get": {
"operationId": "{{../serviceName}}_{{methodName}}",
"parameters": [
{{#args}}
{
"name": "{{argName}}",
"in": "query",
"schema": {"type": "{{openApiType}}"}
}{{^last}},{{/last}}
{{/args}}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"result": {"type": "{{openApiResultType}}"}
}
}
}
}
}
}
}
}{{^last}},{{/last}}
{{/methods}}
{{/services}}
}
}
Once you have the OpenAPI spec, integrate with Swagger UI:
// Add static file serving for Swagger UI
procedure TMyServer.SwaggerUI(Ctxt: TRestServerUriContext);
begin
Ctxt.ReturnFileFromFolder('./swagger-ui/', True, 'index.html');
end;
For bidirectional communication:
class MorMotWebSocket {
constructor(url) {
this.ws = new WebSocket(url);
this.callbacks = new Map();
this.callId = 0;
this.ws.onmessage = (event) => {
const response = JSON.parse(event.data);
const callback = this.callbacks.get(response.id);
if (callback) {
callback(response.result);
this.callbacks.delete(response.id);
}
};
}
call(service, method, params) {
return new Promise((resolve) => {
const id = ++this.callId;
this.callbacks.set(id, resolve);
this.ws.send(JSON.stringify({
id,
method: `${service}.${method}`,
params
}));
});
}
}
// Usage
const ws = new MorMotWebSocket('ws://localhost:8080/api');
const result = await ws.call('Calculator', 'Add', [10, 20]);
For server-to-client callbacks:
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.callback) {
// Server-initiated callback
handleCallback(msg.callback, msg.params);
} else if (msg.id) {
// Response to our request
resolveRequest(msg.id, msg.result);
}
};
function handleCallback(name, params) {
switch (name) {
case 'IProgress.Update':
updateProgressBar(params.percent);
break;
case 'IProgress.Completed':
showComplete(params.success);
break;
}
}
Mobile apps must handle:
// Retry logic with exponential backoff
function CallWithRetry(Client: TRestHttpClientSocket;
const Method: RawUtf8; MaxRetries: Integer = 3): RawJson;
var
Attempt: Integer;
Delay: Integer;
begin
Delay := 100; // Initial delay in ms
for Attempt := 1 to MaxRetries do
try
Result := Client.CallBackGetResult(Method, []);
Exit;
except
on E: Exception do
begin
if Attempt = MaxRetries then
raise;
Sleep(Delay);
Delay := Delay * 2; // Exponential backoff
end;
end;
end;
For offline support:
type
TCachedClient = class
private
fClient: TRestHttpClientSocket;
fCache: TDocVariantData;
public
function GetData(const Key: RawUtf8): Variant;
end;
function TCachedClient.GetData(const Key: RawUtf8): Variant;
begin
// Try cache first
if fCache.GetValueByPath(Key, Result) then
Exit;
// Fetch from server
try
Result := fClient.CallBackGetResult('GetData', ['key', Key]);
fCache.AddValue(Key, Result);
except
// Return cached data even if stale
Result := fCache.Value[Key];
end;
end;
mORMot works with FireMonkey for iOS/Android:
uses
mormot.rest.http.client,
FMX.Forms;
procedure TMainForm.ConnectButtonClick(Sender: TObject);
begin
// Same code works on mobile
fClient := TRestHttpClientSocket.Create(
EditServer.Text,
EditPort.Text,
fModel
);
fClient.ServiceDefine([IMyService], sicShared);
end;
Note: Ensure you're using TRestHttpClientSocket or TRestHttpClientCurl on mobile, not Windows-specific classes.
Include version in your API root:
// Server
Server := TRestServerDB.Create(Model, 'data.db3');
Server.Model.Root := 'api/v1';
// Client
Client := TRestHttpClientSocket.Create('server', '8080',
TOrmModel.Create([], 'api/v1'));
Standardize error responses:
{
"errorCode": 400,
"errorText": "Invalid parameter: n1 must be positive"
}
Handle on client:
async function callService(method: string, params: any): Promise<any> {
const response = await fetch(`${baseUrl}/${method}?${new URLSearchParams(params)}`);
const data = await response.json();
if (data.errorCode) {
throw new Error(`${data.errorCode}: ${data.errorText}`);
}
return data.result;
}
1. Always use HTTPS in production 2. Validate input on server side 3. Use authentication for sensitive operations 4. Rate limiting to prevent abuse 5. CORS headers for browser clients:
HttpServer.AccessControlAllowOrigin := '*'; // Or specific origins
mORMot 2's cross-platform capabilities:
| Previous | Index | Next |
|---|---|---|
| Chapter 16: Client-Server Services via Interfaces | Index | Chapter 18: The MVC Pattern |