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).
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!
You must be logged in to post a comment.