A Delphi Int64 holds any whole number up to 9,223,372,036,854,775,807.
A JavaScript number is a 64-bit floating point value with a 53-bit mantissa, so it holds whole
numbers exactly only up to 253 - 1, which is 9,007,199,254,740,991
(Number.MAX_SAFE_INTEGER). Anything larger is rounded to the nearest value it can
hold.
When a Delphi server sends an Int64 in JSON, Delphi writes every digit
correctly. The damage happens in the browser, when the JSON is parsed. There is no error and no
warning - the value is simply different.
This is the JSON a Delphi server produced for 253 + 1, using
System.JSON:
{"Rights":9007199254740993,"Rights_Hex":"0020000000000001"}
Parsed in the browser by a TMS WEB Core application, Rights came out as
9007199254740992. Bit 0 was lost.
It matters most for values where every bit counts:
TMS WEB Core is affected in the same way as plain JavaScript. pas2js compiles
Int64 and NativeInt to JavaScript numbers, and the XData web client
reads replies with JSON.parse.
A string is never rounded, so send the value as a string. Decimal text would work, but hex is easier to use at the other end:
The server needs two functions: one to write the value and one to read it back when the browser returns it.
unit Hex64;
interface
// Converts an Int64 to exactly 16 hex digits, e.g. 255 -> '00000000000000FF'.
// Negative values come out as their two's complement bit pattern.
function Int64ToHex(const AValue: Int64): string;
// Converts 16 hex digits back to an Int64. Returns False, and AValue = 0,
// unless AHex is exactly 16 hex digits (upper or lower case).
function HexToInt64(const AHex: string; out AValue: Int64): Boolean;
implementation
uses
System.SysUtils;
const
HexLength = 16;
HexDigits = '0123456789ABCDEFabcdef';
function IsHex16(const AHex: string): Boolean;
var
I: Integer;
begin
Result := Length(AHex) = HexLength;
if not Result then
Exit;
for I := 1 to HexLength do
if Pos(AHex[I], HexDigits) = 0 then
Exit(False);
end;
function Int64ToHex(const AValue: Int64): string;
begin
Result := IntToHex(AValue, HexLength);
end;
function HexToInt64(const AHex: string; out AValue: Int64): Boolean;
begin
AValue := 0;
// TryStrToInt64 accepts things like '$1F' or ' 1F', so check the text first
Result := IsHex16(AHex) and TryStrToInt64('$' + AHex, AValue);
if not Result then
AValue := 0;
end;
end.
IsHex16 is there because TryStrToInt64 is generous. It accepts a
leading space, a $ prefix and short values, all of which you want to refuse when
the text came from a browser.
Writing the JSON is then one extra pair:
LObj := TJSONObject.Create;
try
LObj.AddPair('Rights', TJSONNumber.Create(LRights));
LObj.AddPair('Rights_Hex', Int64ToHex(LRights));
Response := LObj.ToJSON;
finally
LObj.Free;
end;
With TMS XData, add a string property to the class you return and fill it with
Int64ToHex. If the value comes straight from a MySQL table through Aurelius, let the
database do it with a generated column:
ALTER TABLE user_roles
ADD COLUMN Rights_Hex varchar(16)
GENERATED ALWAYS AS (LPAD(HEX(Rights), 16, '0')) VIRTUAL;
MySQL's HEX treats a BIGINT as 64 unsigned bits, so -1 becomes
FFFFFFFFFFFFFFFF, the same as IntToHex. Map the column as read-only
in the entity:
[Column('Rights_Hex', [TColumnProp.NoInsert, TColumnProp.NoUpdate], 16)]
FRights_Hex: string;
In the browser the 64 bits are held as two numbers of 32 bits each. Each half is at most 4,294,967,295, far below the 253 limit, so nothing is ever rounded.
unit Bits64;
interface
type
// 64 bits held as two 32-bit halves, each a plain number 0..4294967295.
// Bits are tested with arithmetic, never with and/or/shl.
TBits64 = record
private
FLo: NativeInt; // bits 0..31
FHi: NativeInt; // bits 32..63
public
procedure Clear;
function IsSet(ABit: Integer): Boolean;
procedure Include(ABit: Integer);
procedure Exclude(ABit: Integer);
function ToHex: string;
// False, with every bit cleared, unless AHex is exactly 16 hex digits
function FromHex(const AHex: string): Boolean;
end;
implementation
uses
SysUtils;
const
HexLength = 16;
HalfHexLength = 8;
BitsPerHalf = 32;
HexDigits = '0123456789ABCDEFabcdef';
function PowerOf2(ABit: Integer): NativeInt;
var
I: Integer;
begin
Result := 1;
for I := 1 to ABit do
Result := Result * 2;
end;
function BitIsSet(const AHalf: NativeInt; ABit: Integer): Boolean;
begin
Result := (AHalf div PowerOf2(ABit)) mod 2 = 1;
end;
function IsHex16(const AHex: string): Boolean;
var
I: Integer;
begin
Result := Length(AHex) = HexLength;
if not Result then
Exit;
for I := 1 to HexLength do
if Pos(AHex[I], HexDigits) = 0 then
Exit(False);
end;
procedure TBits64.Clear;
begin
FLo := 0;
FHi := 0;
end;
function TBits64.IsSet(ABit: Integer): Boolean;
begin
if ABit < BitsPerHalf then
Result := BitIsSet(FLo, ABit)
else
Result := BitIsSet(FHi, ABit - BitsPerHalf);
end;
procedure TBits64.Include(ABit: Integer);
begin
if IsSet(ABit) then
Exit;
if ABit < BitsPerHalf then
FLo := FLo + PowerOf2(ABit)
else
FHi := FHi + PowerOf2(ABit - BitsPerHalf);
end;
procedure TBits64.Exclude(ABit: Integer);
begin
if not IsSet(ABit) then
Exit;
if ABit < BitsPerHalf then
FLo := FLo - PowerOf2(ABit)
else
FHi := FHi - PowerOf2(ABit - BitsPerHalf);
end;
function TBits64.ToHex: string;
begin
Result := IntToHex(FHi, HalfHexLength) + IntToHex(FLo, HalfHexLength);
end;
function TBits64.FromHex(const AHex: string): Boolean;
var
LHi, LLo: NativeInt;
begin
Result := IsHex16(AHex) and
TryStrToInt('$' + Copy(AHex, 1, HalfHexLength), LHi) and
TryStrToInt('$' + Copy(AHex, HalfHexLength + 1, HalfHexLength), LLo);
if Result then
begin
FHi := LHi;
FLo := LLo;
end
else
Clear;
end;
end.
Using it after an XData call or any other JSON reply:
var
LRights: TBits64;
begin
if not LRights.FromHex(string(LReply['Rights_Hex'])) then
raise Exception.Create('Invalid rights from server');
if LRights.IsSet(Ord(rView_Bookings)) then
ShowBookingsMenu;
// Change a bit and send the hex back; the server reads it with HexToInt64
LRights.Include(Ord(rEdit_Bookings));
SaveRights(LRights.ToHex);
end;
In plain JavaScript the bitwise operators &, | and
<< work on 32-bit signed integers, so 2147483648 & 2147483648
gives -2147483648. pas2js does better than that: on NativeInt it compiles
and, or and shl into helper functions
(rtl.and, rtl.shl) that work correctly up to 53 bits. In a test,
2147483648 and 2147483648 gave 2147483648 and (4294967296 + 5) and
4294967296 gave 4294967296.
So inside the 53-bit range you could use and in Pascal code. The record uses
div and mod anyway, for two reasons. The same logic works in an
asm block or a JavaScript library where the operators really are 32-bit, and it does
not depend on a variable being declared NativeInt rather than
Integer. Neither approach can hold all 64 bits in one number, which is why the
record keeps two halves.
Both units were compiled and run before this article was published.
High(Int64),
Low(Int64), 253 + 1 and 200,000 random values, plus rejection of short,
long, prefixed and non-hex input.FromHex, rebuilt bit by bit and compared, and the same bad input rejected.HexToInt64 refuses
anything that is not exactly 16 hex digits.Int64 on the server. If negative values mean something else in your
code, keep bit 63 unused.
Please donate if helpful