Home > Delphi, JavaScript, Object Pascal, OP4JS, Smart Mobile Studio > Smart Mobile Studio and low level memory access!

Smart Mobile Studio and low level memory access!

For the past 7 .. 8 days I have spent most of my spare time working on two particular units: system.interop and smartcl.legacy. I don’t think I have ever spent so much time on a unit (at least not under SMS) for many years. Not because it’s super hard to code these, but because in order to arrive at the best solution – you often have to evolve your ideas through different approaches. You can read my previous post about this here.

But the journey has been worth it. Let me explain why

Memory allocations? But JavaScript doesn’t do that!

System.Interop, which takes its name from the .net framework, is all about code that facilitates interoperability; meaning classes and functions which helps you talk to other systems. Since “other systems” in many cases means native code, being able to work with untyped-memory (which JavaScript does support) and typed-array abstractions (called views, which conceptually functions like pointers) is essential.

I was able to port over both CRC and RLE compression
directly from my Delphi codebase without much change

I am happy to inform you that Smart Mobile Studio now supports a TMemory class, which encapsulates a fully untyped piece of memory. It extends ordinary and boring untyped-memory buffers with the ability to grow, shrink, scale and much more. All in all TMemory has around 50 methods for manipulating memory, so you are well catered for in that department.

So if you are half expecting some sloppy string buffer or something like that, you should be pleasantly surprised.

There is also a class called TDatatype which provides methods for converting intrinsic (built-in) datatypes to bytes, and bytes back to an intrinsic datatype. So you have handy single shot functions like Int32ToBytes, Float32ToBytes and naturally their counterparts (BytesToInt32, BytesToFloat32).

The problem is not the propblem, the problem is your attitude towards the problem.. savvy?

The problem is not the propblem, the problem is your attitude towards the problem.. savvy?

On top of this system we finally reach the pretty high level interface, namely streams. I write “finally” because this has been quite a chore. The central problem is not the browser, nor is it me. The central problem is that the wide majority of JavaScript developers blogging about their code doesn’t have a clue what they are talking about. A whole generation of programmers who never learnt about pointers at school or university (!) An intellectual emergency of epic proportions.

you can now use AllocMem() and the other classical memory management
functions just like you would under Freepascal and Delphi

Now dont get me wrong, I have seen some fantastic code out there and JavaScript is really the language which is seeing more growth than any of the others combined, but if you want to work with untyped memory and accessing them through “views” (read: typed pointers) then you preferably should have some experience with pointers first.

With my 20+ year background in programming I had no problems navigating the docs (once I found them), and although it took me a few days to sift through all the rubbish, separating the good bad, I feel we have truly power-house of a solution on our hands.

Speed

If you think JS is to slow for anything serious, think again! People were shocked when Delphi was humiliated by JavaScript graphics back in 2012. It turns out JavaScript is just faster than compiled Delphi (in this case Delphi 7 .. Delphi 2006). This was a wake-up call for many developers I think.

for x:=0 to 40000 do
begin
  mWriter.WriteInteger($AAAA0000);
  mWriter.WriteFloat64(19.24);
  mWriter.WriteFloat64(24.19);
  mWriter.WriteBoolean(false);
  mWriter.WriteInteger($0000AAAA);
end;

RECORDS  = 40.000
BYTES    = 29 bytes * RECORDS = 1,160.225 Megabytes
CACHE    = 4096 bytes
=======================
DURATION = 2.63 seconds

NOTE: The example above writes through a 4-layer architecture: StreamWriter to stream-object, stream-object to buffer via TDatatype (datatype conversion to bytes), buffer writes to memory. This is one of the more time consuming tests, since the dynamic nature of a stream forces the buffer to re-allocate and move the content frequently.

If we go below the stream architecture, straight to the TMemory class, well moving data around inside is blistering fast (the example above moves data through 4 different objects, which is the price to pay for a good framework). The reason low-level memory operations are fast is because un-typed buffers are allocated “as is” in memory; As a single block with a related pointer defined for the data. So forget everything you know about ordinary arrays under JavaScript;

PS: True pointers under JavaScript are called Typed-Arrays, and serve as a view or window into the allocated memory according to it’s type. So if you use an Int32 type you will naturally be able to access the memory as an array of integers. If you chose a byte pointer (UInt8Array) and connect that to the buffer – you can manipulate the buffer on byte level.

The hard part is creating an infrastructure around these methods, a bridge between intrinsic types and typed/un-typed memory. Which is exactly what System.Interop gives you.

Allocmem? No way!

My SmartCL.Legacy unit is all about helping you move your Delphi or FreePascal code and skill-set to Smart Pascal and HTML5. Since one of the major differences between FPC/Delphi and HTML5 is how graphics are drawn (the HTML5 canvas object bears little resemblence to good old TCanvas) I decided to create a full clone of our old belowed TCanvas. So if you have a ton of working Object Pascal code for drawing graphics around the place, you should now be able to re-cycle it under HTML5 with little or no change.

The same goes for TBitmap, a class that I must admit I have missed myself from time to time. It’s easy to create a graphics context and a TW3Canvas, but old habits die hard and TBitmap still has it’s uses. Even though we have been complaining about it’s limitations for decades 🙂

But what about classical pascal concepts like AllocMem(), FreeMem(), Move() and FillChar() I hear you say?

Well you may think I am joking but I implemented that as well! The hairs on your neck should stand up right about now. Yes you read right, you can now use AllocMem() and the other classical memory management functions just like you would under Freepascal and Delphi.

Dealing with pointers

If you know your JavaScript you may be thinking “But what pointers? JavaScript doesn’t have pointers!”. That is very true indeed. JavaScript doesnt have a type called pointer. So in order to provide such a type, we have to adopt some inspiration from C#: meaning that a pointer under Smart Mobile Studio is actually an object. This technique is called “marshaling”.

Here is how you allocate memory and play around with it:

procedure TForm1.testMemory;
var
  mData: TAddress;
begin
  mData:=TMarshal.Allocmem(1024);
  try
    //fill the buffer with zero
    TMarshal.FillChar(mData,1024,#0);

    //Fill 200 bytes, starting at offset 100, with the letter "A";
    TMarshal.FillChar(mData.Addr(100),200,"A");

    //move 200 bytes, starting at offset 200, to position 400
    TMarshal.Move(mData.Addr(200),mData.Addr(400),200);
  finally
    freemem(mData);
  end;
end;

Pretty cool right? And yes, AllocMem() really does allocate untyped memory. TAddress is just a very, very thin piece of code which serves as a “reference point” into the allocated untyped memory.

The result? Well, I was able to port over both CRC and RLE compression directly from my Delphi codebase without much change.

Marshaling

like mentioned, keeping references to memory through handles and offsets is called marshaling. It’s more or less the same technique as used by CLR under .NET, and before that Java and Visual Basic (and many more languages as well). So these languages have gotten around their initial lack of memory access without to much penalty. I mean – games like MineCraft is written in 100% Java and runs just as fast as native C++ despite the lack of pointers. As does the .NET framework (note: the “lack” of pointers is not really true for the .NET framework, but pointer operations are regarded as “unsafe”).

Marshaling simply means that TAddress contains a reference to the un-typed memory, and also an offset into this memory buffer (see TAddress.Entrypoint property). The “real” memory is only accessed through the operations which act on the memory, and only then through typed-views which ensure safety (sigh).

Let’s take a peek under the hood of TAddress

  TAddress  = Class
  private
    FOffset:    Integer;
    FBuffer:    THandle;
  protected
    function    _getSegRef:THandle;virtual;
  public
    Property    &Type:JUInt8ClampedArray
                read ( new JUInt8ClampedArray(JTypedArray(_getSegRef)) );
    Property    Entrypoint:Integer read FOffset;
    Property    Segment:THandle read FBuffer;
    function    Addr(const Index:Integer):TAddress;
    function    Reflect:TMemory;
    Constructor Create(const aSegment:THandle;
                const aEntrypoint:Integer);virtual;
    Destructor  Destroy;Override;
  end;

//############################################################################
// TAddress
//############################################################################

Constructor TAddress.Create(const aSegment:THandle;
            const aEntrypoint:Integer);
begin
  inherited Create;
  if (aSegment) then
  FBuffer:=aSegment else
  Raise EAddress.Create('Failed to derive address, invalid segment error');

  if aEntryPoint>=0 then
  FOffset:=aEntryPoint else
  Raise EAddress.Create('Failed to derive address, invalid entrypoint error');
end;

Destructor TAddress.Destroy;
begin
  FBuffer:=NIL;
  FOffset:=0;
  inherited;
end;

function TAddress._getSegRef:THandle;
begin
  result:=JTypedArray(FBuffer).buffer;
end;

function TAddress.Addr(const Index:Integer):TAddress;
var
  mTarget:  Integer;
begin
  if Index>=0 then
  Begin
    mTarget:=FOffset + Index;
    if (mTarget>=0) and (mTarget < JUint8ClampedArray(FBuffer).byteLength) then
    result:=TAddress.Create(FBuffer,mTarget) else
    raise EAddress.Create
    ('Failed to derive address, entrypoint exceeds segment bounds error');
  end else
  Raise EAddress.Create
  ('Failed to derive address, invalid entrypoint error');
end;

function TAddress.Reflect:TMemory;
begin
  if (FBuffer) then
  result:=TMemory.Create(FBuffer.buffer) else
  Raise EAddress.Create('Failed to reflect memory, null reference error');
end;

As you can see from the code above, TAddress is extremely efficient and thin. It only manages the entrypoint into the associated memory-segment, the rest is very simple but effective maintenance code.

Let’s take a closer look under the hood of TMarshal while we are at it:

//############################################################################
// TMarshal
//############################################################################

class procedure TMarshal.FillChar(const Target:TAddress;
      const Size:Integer;
      const Value:Byte);
var
  mSegment: JUint8ClampedArray;
  mIndex:   Integer;
Begin
  if Target<>NIl then
  begin
    mSegment:=JUint8ClampedArray( Target.Segment );
    if mSegment<>NIL then
    Begin
      mIndex:=Target.Entrypoint;
      TMemory.Fill(Target.Segment,mIndex,Size,Value);
    end;
  end;
end;

class Procedure TMarshal.FillChar(const Target:TAddress;
      const Size:Integer;
      const Value:String);
var
  mSegment: JUint8ClampedArray;
  mByte:    Byte;
Begin
  if Target<>NIl then
  begin
    if Value.length>0 then
    begin
      mByte:=TDataType.CharToByte(Value);
      mSegment:=JUint8ClampedArray( Target.Segment );
      if mSegment<>NIL then
      TMemory.Fill(Target.Segment,Target.Entrypoint,Size, mByte);
    end;
  end;
end;

class procedure TMarshal.Move(const Source:TAddress;
          const Target:TAddress;const Size:Integer);
Begin
  if Source<>NIL then
  Begin
    if Target<>NIl then
    begin
      if Size>0 then
      TMemory.Move(Source.segment,Source.Entrypoint,
      target.segment,target.entrypoint,Size);
    end;
  end;
end;

class procedure TMarshal.ReAllocmem(var Segment:TAddress;
                const Size:Integer);
var
  mTemp:  TAddress;
  mSize:  Integer;
begin
  if segment<>NIL then
  begin
    mSize:=JUint8ClampedArray(segment.Segment).length;
    mTemp:=AllocMem(Size);
    case (Size>mSize) of
    true:   move(segment,mtemp,mSize);
    false:  move(segment,mTemp,Size);
    end;

    //Ensure reference is picked up by garbage collector
    SegMent.free;
    Segment:=NIL;
    Segment:=mTemp;
  end else
  SegMent:=AllocMem(Size);
end;

class function TMarshal.AllocMem(Const Size:Integer):TAddress;
var
  mBuffer:  JArrayBuffer;
  mArray:   JUint8ClampedArray;
begin
  result:=NIL;
  if Size>0 then
  Begin
    mBuffer:=new JArrayBuffer(Size);
    asm
    @mArray = new Uint8ClampedArray(@mBuffer);
    end;
    result:=TAddress.Create(mArray,0);
  end;
end;

class procedure TMarshal.FreeMem(Const Segment:TAddress);
begin
  if Segment<>NIL then
  Segment.free;
end;

You could be excused to think that this can hardly provide such advanced features, and you are right. The infrastructure which does the actual moving of data or population of data is inside the TMemory class. Which is far to big to post here.

Well, I hope I have wetted your appetite for cutting edge HTML5 development with Smart Mobile Studio!

Enjoy!

Advertisements
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: