A TMS WEB Core front end talking to a Delphi server usually needs the same definitions on both sides: enums, status rules, validation, the shape of the records that go over the wire. Writing them twice means they drift apart. Writing them once means the unit has to compile with two different compilers - Delphi's own for the server, and pas2js, which TMS WEB Core uses to turn Pascal into JavaScript.
pas2js accepts most modern Object Pascal, but not all of it, and a few things that do compile behave differently in the browser. Every result below was checked by compiling the code with Delphi 13 (Win32 and Win64) and with the pas2js compiler in TMS WEB Core, and running it.
| Construct | pas2js | Use instead |
|---|---|---|
AnsiChar | identifier not found | Char, or an alias - see below |
ShortString, string[20] | not found / syntax error | string |
AnsiString | identifier not found | string |
PChar | identifier not found | string and indexes |
Variant | identifier not found | JSValue (in unit JS) |
SizeOf | identifier not found | nothing - there is no memory layout to measure |
Variant records (case ... of in a record) | not supported | separate fields, or functions that convert |
These compile and work in both: Pointer, absolute,
Extended (it is a Double in the browser), with,
TryStrToInt64, generics such as TList<Integer>, records with methods
and static class functions, class variables, sets of enums, and dotted unit names
such as System.SysUtils.
pas2js defines these symbols itself:
PAS2JS - always, so use this one to test for the browserECMASCRIPT - the JavaScript targetWEBLIB - defined by TMS WEB CoreUNICODE - as in Delphi 2009 and laterMSWINDOWS is not defined in the browser. A define of your own added in the TMS
project options also works, but PAS2JS is there in every project without anyone
having to remember to add it.
{$IFDEF PAS2JS}
// browser only
{$ELSE}
// Delphi only
{$ENDIF}
In the browser Int64 and NativeInt are JavaScript numbers.
High(Int64) is 9,007,199,254,740,991 (253 - 1) instead of
9,223,372,036,854,775,807. Two consequences, both seen in testing:
I64 := 9007199254740993; gives
Incompatible types: got "Double" expected "NativeInt".9007199254740991 + 2 gave 9007199254740992.Shared code that handles IDs, bit masks or totals that can exceed 253 must not rely
on Int64 in the browser. The article
Sending Int64
Values to JavaScript shows how to carry them as hex.
In Delphi, Currency is a 64-bit integer scaled by 10,000, so 23.99 is stored as
239900 exactly. pas2js keeps the same scale but stores it in a JavaScript number, and converting
a decimal value into that scale is not rounded. In the generated JavaScript the literal
23.99 became 239899.99999999997.
Values built from whole numbers stay exact - quantity times a unit price added up to exactly 239900. Values that start as decimal text do not:
N / 100. In Delphi, none did.23.99, Currency(23.99), a typed constant of 23.99 or
StrToCurr('23.99'). All four compared equal in Delphi.Compare money in whole cents, which gave the right answer on every platform:
if Round(Total * 100) = Round(Expected * 100) then
...
The same applies to amounts that arrive in JSON, because the browser reads them as decimal numbers. Keeping amounts as integer cents across the wire avoids the question altogether.
Code that deliberately uses AnsiChar - for example a set of AnsiChar
constant - can keep it on the server by giving pas2js an alias:
{$IFDEF PAS2JS}
type
AnsiChar = Char; // pas2js has no single-byte character type
{$ENDIF}
Keep the alias inside {$IFDEF PAS2JS}. Left active for desktop compilers it turns
every AnsiChar in every unit that uses it into a 2-byte Char; the
article on W1050 shows the
warning that gives this away.
This unit holds order rules used by both a Delphi server and a TMS WEB Core client. Apart
from the alias, nothing in it needs a {$IFDEF}.
unit Order_Rules;
// Order rules shared by the Delphi server and the TMS WEB Core client.
// Compiles with dcc32/dcc64 and with pas2js.
interface
uses
System.SysUtils;
{$IFDEF PAS2JS}
type
AnsiChar = Char; // pas2js has no single-byte character type
{$ENDIF}
type
TOrderStatus = (osDraft, osPlaced, osPaid, osShipped, osCancelled);
TOrderStatuses = set of TOrderStatus;
TOrderLine = record
Product: string;
Quantity: Integer;
UnitPrice: Currency;
function Total: Currency;
end;
TOrderLines = array of TOrderLine;
const
StatusNames: array [TOrderStatus] of string =
('Draft', 'Placed', 'Paid', 'Shipped', 'Cancelled');
// Statuses an order may move to from each status
NextStatuses: array [TOrderStatus] of TOrderStatuses = (
[osPlaced, osCancelled], // from Draft
[osPaid, osCancelled], // from Placed
[osShipped, osCancelled], // from Paid
[], // from Shipped
[]); // from Cancelled
MaxOrderCodeLength = 12;
function CanMoveTo(const AFrom, ATo: TOrderStatus): Boolean;
function OrderTotal(const ALines: TOrderLines): Currency;
// Order codes are 3 to 12 characters: letters, digits and '-' only
function IsValidOrderCode(const ACode: string): Boolean;
function StatusFromName(const AName: string; out AStatus: TOrderStatus): Boolean;
implementation
const
CodeChars: set of AnsiChar = ['A'..'Z', 'a'..'z', '0'..'9', '-'];
MinOrderCodeLength = 3;
function TOrderLine.Total: Currency;
begin
Result := Quantity * UnitPrice;
end;
function CanMoveTo(const AFrom, ATo: TOrderStatus): Boolean;
begin
Result := ATo in NextStatuses[AFrom];
end;
function OrderTotal(const ALines: TOrderLines): Currency;
var
I: Integer;
begin
Result := 0;
for I := 0 to High(ALines) do
Result := Result + ALines[I].Total;
end;
function IsValidOrderCode(const ACode: string): Boolean;
var
I: Integer;
begin
Result := (Length(ACode) >= MinOrderCodeLength) and
(Length(ACode) <= MaxOrderCodeLength);
if not Result then
Exit;
for I := 1 to Length(ACode) do
if not CharInSet(ACode[I], CodeChars) then
Exit(False);
end;
function StatusFromName(const AName: string; out AStatus: TOrderStatus): Boolean;
var
S: TOrderStatus;
begin
for S := Low(TOrderStatus) to High(TOrderStatus) do
if SameText(StatusNames[S], AName) then
begin
AStatus := S;
Exit(True);
end;
AStatus := osDraft;
Result := False;
end;
end.
The browser greys out buttons with CanMoveTo, and the server refuses the same
moves with the same function, so the two can never disagree.
Keep shared units in a folder of their own, outside both projects, and add the unit to each
project explicitly - in the uses clause of the .dpr with an
in path, which also puts it in the .dproj:
uses
Order_Rules in '..\shared\Order_Rules.pas',
Relying on the search path works until the same file name exists in two folders. An explicit path makes it certain both compilers use the same copy.
{$IFDEF PAS2JS} is a second version of
the code that only gets tested when someone builds that side.absolute tricks that turn
a set into integers depend on memory layout, which the browser does not have. Convert with
functions instead.
Please donate if helpful