Pacific Blue Software Logo

Sharing Units Between VCL and TMS WEB Core (pas2js)

Sharing Units Between VCL and TMS WEB Core (pas2js)

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.

What pas2js Does Not Accept

Constructpas2jsUse instead
AnsiCharidentifier not foundChar, or an alias - see below
ShortString, string[20]not found / syntax errorstring
AnsiStringidentifier not foundstring
PCharidentifier not foundstring and indexes
Variantidentifier not foundJSValue (in unit JS)
SizeOfidentifier not foundnothing - there is no memory layout to measure
Variant records (case ... of in a record)not supportedseparate 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.

Telling the Compilers Apart

pas2js defines these symbols itself:

MSWINDOWS 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}

Int64 Is Only 53 Bits

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:

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.

Currency Is Not Exact in the Browser

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:

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.

The AnsiChar Alias

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.

A Shared Unit

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.

Adding It to Both Projects

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.

Rules That Keep It Working

Testing


PBS Delphi - One Unit for the Server and the Browser


Back to Articles for Developers
Back to Articles on Delphi
How to Send Int64 Values from Delphi to JavaScript
Interposer Classes in Delphi
How to Stop the Red Error Panel in TMS WEB Core

If you found this useful, then please consider making a donation.

paypal
QR Code for donation Please donate if helpful