Universal Code with Oxygene
In this post we are going to look at creating universal code that can be compiled with the RemObjects Elements compiler to work on all our supported targets.
But first, let me introduce myself. My name is Jon Lennart Aasenden. I have been a Pascal enthusiast since the early 90's. I keep active on Social Media and manage most of the Object-Pascal groups on Facebook and the MeWe network. I have also started our new group, RemObjects Developer (so if you are on Facebook, join our community!).
My background is quite varied, but I have mostly worked on low-level stuff and system services. I am the founder of Smart Pascal together with Eric Grange, which resulted in a product called Smart Mobile Studio.
Before I joined RemObjects I worked at Embarcadero as the SC (Sales and Software Consultant), covering Europe, the Middle East and Africa (EMEA). I am situated in the ancient and beautiful city of Tønsberg, Norway.
Joining RemObjects is super exciting! Since I have been involved in creating compilers and languages myself, having a front-seat view at RemObjects is pretty much Xmas 24/7 for me. RemObjects is a small company, but a highly productive one. And working close to the metal is what I love.
Universal Code?
When you hear universal code your thoughts might conjure up notions of bytecodes, like something produced by a Java compiler or a .Net compiler. Those can indeed also be regarded as universal, providing there is an engine available to process and execute that code.
The code that I define as universal though, is code that is written in such a way that our Oxygene Pascal compiler can use the code anywhere, regardless of target. The rules for such generic code are simple:
- Avoid pointers
- Stick to intrinsic data-types
- No system calls
Just to be clear: pointers are supported by Oxygene, even if you compile for WebAssembly ("wasm"). But as with all disciplines, what you can do and what you should do are two different things. I try to avoid pointers as much as possible to maximize portability.
The immediate benefit of writing code that sticks to these rules is that you can easily move it between compilers with little or no change. The code I will present here was originally written for Delphi, with heavy use of pointers. I then ported it to Smart Pascal, which only supports blue-pointers (a technique for using offsets into a pre-allocated buffer), and now finally I ported it to Oxygene.
It took only 10 minutes to make the code run under Oxygene, because I had already gotten rid of things that could potentially be a showstopper.
Mixing it up
One of the cool things about the Elements compiler suite is that you are not limited to just one language. Perhaps you have some code written in C# that you want to use? Java maybe? Swift? Well, with Elements you can mix and match units written in different languages.
And this is the magic, because even if you use C#, you are not bound to the .NET Framework. I mean, Object Pascal as a language is not defined by the VCL (Delphi) or the LCL (Free Pascal). Elements provides its own minimal RTL, so the language is separated from the framework and target. You can read more about the Elements RTL here: https://www.elementscompiler.com/elements/rtl.aspx
Now, it can take a little while to adjust to all this, but when the potential hits you, it hits you hard. I was speechless when I realized what we could do with this. I could take advantage of all the cool features C# has to offer, then re-apply my skills as a Delphi developer - and write code that targets Java, .NET, or raw, optimized code for ARM Linux even! That is pretty exciting stuff!
BTree lookup class
Every developer has his own "toolbox". A set of units and code that he or she have polished over many years; code that you use in many different projects. Well, since I'm new to Oxygene, the best way to get into the dialect is to port over some code. And I started with a BTree (binary tree) class:
In computer science, a B-tree is a self-balancing tree data structure that maintains sorted data and allows searches, sequential access, insertions, and deletions in logarithmic time. -Wikipedia
To sum it up: BTree is a super-fast lookup-table. You can register as many items as you want, providing each item has a unique identifier. When you later want to find an item - BTree locates it incredibly fast!
I have used this in database tables, games, search engines and collections. It is a wonderfully simple way to keep track of things -with logarithmic response time. In short: it costs very little CPU power, yet delivers incredible performance in searches.
Right, let's have a look at the code!
namespace BTree;
interface
type
BTreeNode = public class(Object)
public
Identifier: Int32;
Data: Object;
Left: BTreeNode;
Right: BTreeNode;
end;
EBTreeError = public class(Exception);
BTreeProcessCB = public block(const Node: BTreeNode; var Cancel: Boolean) of object;
BTree = public class(Object)
private
FRoot: BTreeNode;
FCurrent: BTreeNode;
public
property Root: BTreeNode read FRoot;
property &Empty: Boolean read (FRoot = nil);
function &Add(const Ident: Int32; const Data: dynamic): BTreeNode;
function &Contains(const Ident: Int32): Boolean;
function &Remove(const Ident: Int32): Boolean;
function &Read(const Ident: Int32): dynamic;
procedure &Write(const Ident: Int32; const NewData: dynamic);
procedure Clear;
function Size: Int32;
procedure ForEach(const Process: BTreeProcessCB);
end;
implementation
function BTree.&Add(const Ident: Int32; const Data: dynamic): BTreeNode;
begin
var LNode := new BTreeNode;
LNode.Identifier := Ident;
LNode.Data := Data;
if FRoot = nil then
FRoot := LNode;
FCurrent := FRoot;
while true do
begin
if (Ident < FCurrent.Identifier) then
begin
if (FCurrent.Left = nil) then
begin
FCurrent.Left := LNode;
break;
end else
FCurrent := FCurrent.Left;
end else
if (Ident > FCurrent.Identifier) then
begin
if (FCurrent.Right = nil) then
begin
FCurrent.Right := LNode;
break;
end else
FCurrent := FCurrent.Right;
end else
break;
end;
result := LNode;
end;
function BTree.&Contains(const Ident: Int32): Boolean;
begin
if FRoot <> nil then
begin
FCurrent := FRoot;
while ( (not Result) and (FCurrent <> nil) ) do
begin
if (Ident < FCurrent.Identifier) then
FCurrent := FCurrent.Left
else
if (Ident > FCurrent.Identifier) then
FCurrent := FCurrent.Right
else
begin
Result := true;
end
end;
end;
end;
function BTree.&Remove(const Ident: Int32): boolean;
var
LFound: Boolean;
LParent: BTreeNode;
LReplacement,
LReplacementParent: BTreeNode;
begin
FCurrent := FRoot;
while (not LFound) and (FCurrent<>nil) do
begin
if (Ident < FCurrent.Identifier) then
begin
LParent := FCurrent;
FCurrent:= FCurrent.Left;
end else
if (Ident > FCurrent.Identifier) then
begin
LParent := FCurrent;
FCurrent := FCurrent.Right;
end else
LFound := true;
if (LFound) then
begin
var LChildCount := 0;
if (FCurrent.Left <> nil) then
inc(LChildCount);
if (FCurrent.Right <> nil) then
inc(LChildCount);
if (FCurrent = FRoot) then
begin
case (LChildCount) of
0: begin
FRoot := nil;
end;
1: begin
FRoot := if FCurrent.Right = nil then
FCurrent.Left
else
FCurrent.Right;
end;
2: begin
LReplacement := FRoot.Left;
while (LReplacement.Right <> nil) do
begin
LReplacementParent := LReplacement;
LReplacement := LReplacement.Right;
end;
if (LReplacementParent <> nil) then
begin
LReplacementParent.Right := LReplacement.Left;
LReplacement.Right := FRoot.Right;
LReplacement.Left := FRoot.Left;
end else
LReplacement.Right := FRoot.Right;
end;
end;
FRoot := LReplacement;
end else
begin
case LChildCount of
0: if (FCurrent.Identifier < LParent.Identifier) then
LParent.Left := nil else
LParent.Right := nil;
1: if (FCurrent.Identifier < LParent.Identifier) then
begin
if (FCurrent.Left = NIL) then
LParent.Left := FCurrent.Right else
LParent.Left := FCurrent.Left;
end else
begin
if (FCurrent.Left = NIL) then
LParent.Right := FCurrent.Right else
LParent.Right := FCurrent.Left;
end;
2: begin
LReplacement := FCurrent.Left;
LReplacementParent := FCurrent;
while LReplacement.Right <> nil do
begin
LReplacementParent := LReplacement;
LReplacement := LReplacement.Right;
end;
LReplacementParent.Right := LReplacement.Left;
LReplacement.Right := FCurrent.Right;
LReplacement.Left := FCurrent.Left;
if (FCurrent.Identifier < LParent.Identifier) then
LParent.Left := LReplacement
else
LParent.Right := LReplacement;
end;
end;
end;
end;
end;
result := LFound;
end;
function BTree.&Read(const Ident: Int32): dynamic;
begin
FCurrent := FRoot;
while FCurrent <> nil do
begin
if (Ident < FCurrent.Identifier) then
FCurrent := FCurrent.Left
else
if (Ident > FCurrent.Identifier) then
FCurrent := FCurrent.Right
else
begin
result := FCurrent.Data;
break;
end
end;
end;
procedure BTree.&Write(const Ident: Int32; const NewData: dynamic);
begin
FCurrent := FRoot;
while (FCurrent <> nil) do
begin
if (Ident < FCurrent.Identifier) then
FCurrent := FCurrent.Left
else
if (Ident > FCurrent.Identifier) then
FCurrent := FCurrent.Right
else
begin
FCurrent.Data := NewData;
break;
end
end;
end;
procedure BTree.Clear;
begin
// just decouple the references and let the GC handle it
FCurrent := nil;
FRoot := nil;
end;
function BTree.Size: Int32;
var
LCount: Int32;
begin
ForEach( procedure (const Node: BTreeNode; var Cancel: Boolean)
begin
inc(LCount);
Cancel := false;
end);
result := LCount;
end;
procedure BTree.ForEach(const Process: BTreeProcessCB);
function ProcessNode(const Node: BTreeNode): Boolean;
begin
if Node <> nil then
begin
if Node.Left <> nil then
begin
result := ProcessNode(Node.Left);
if result then
exit;
end;
Process(Node, result);
if result then
exit;
if (Node.Right <> nil) then
begin
result := ProcessNode(Node.Right);
if result then
exit;
end;
end;
end;
begin
ProcessNode(FRoot);
end;
end.
Using the BTree class
You can freely copy the code above and use it in your own project. Simply replace the namespace with whatever you like, it's free code and you can use and abuse it as you see fit.
Using the BTree code in your projects is really simple. And I'm sure you can see ample room to expand it. But to start simple:
var lLookup := new BTree();
lLookup.Add($1200, "Hello World");
lLookup.Add($1300, "This is cool");
var lData := lLookup.Read($1300);
ShowMessage(lData.ToString());
One thing to notice about the class is that the data it keeps track of is dynamic. This is a special data-type that can resemble variant in Delphi or Free Pascal, meaning that it can take whatever data you give it. I must stress that dynamic has none of the limitations that Delphi or Free Pascal variants have, there is no type mapping going on behind the scenes - and it's not bound to the ancient COM standard that Delphi variants were modeled on. It basically means "anything".
So, in the example above we first create an instance of the class:
var lLookup := new BTree();
Then we register two data items, just two normal strings, each with its own identifier. Please note that you can store whatever you like here, a string, a record, a class instance – anything goes:
lLookup.Add($1200, "Hello World");
lLookup.Add($1300, "This is cool");
And finally, we use the read() method to quickly locate item number two, with the $1300 identifier. Again, we could register 10.000 items, the BTree class would locate the item incredibly fast!
var lData := lLookup.Read($1300);
ShowMessage(lData.ToString());
In the code above we tell BTree to return the item with the identifier $1300. The variable LData will get the data, and then we display it using the ShowMessage() procedure.
This code can be used anywhere, be it Windows, OS X, Arm Linux, Java, .Net. It is well within the rules I outlined at the beginning of this post, and even ran on the first try under WebAssembly:
Final Words
I hope you have found this post informative. I am super excited about Oxygene, and the whole Elements toolchain is incredibly impressive!
In the weeks and months to come I will port over more and more code and demonstrate just how powerful RemObjects technology is. The development tools is just one aspect of what RemObjects delivers, so writing code that talks to Remoting SDK and Data Abstract is also on the menu!
Stay tuned and let's explore Elements together!