Data Transmission with Delphi Redux
In this blog post I will revisit my series from four years ago and update it with more current technology
Revisiting the series that deal with the transfer of raw data at high speed with direct access using pointers to structures that can be supported by most programming platforms.
Why am I revisiting a four year old post?
I was never happy with the solution to get default values based on the generic type and new functionality in Delphi has allowed me to clean up some of the code I wrote for Part 3 of my series on data transmission.
I want to share what I found, how it can clean up the code from the prior article and potential for other uses in your own projects.
In case you missed it
I finished my original series of three articles exactly four years ago to the date. If you missed it here are some links to the original posts.
- Part 1: Pointers and Structures
- Part 2: Arrays and Pointer Arithmetic
- Part 3: Transmitting and Interpreting Data
Here is the gist for those that don’t want to read that much: If two parties share a pointer and some common understanding of the structure of the memory at that location, then we can have very fast communication. Since arrays are contiguous blocks of memory we can rapidly advance through memory and read structures provided that we know the size of each of the structures, their composition and the number of structures we need to read.
The end result was a library example using closures to configure records for transmission. In my example I created a transmitter TTxer
that can send any of a number of geometry entity records defined using generics.
Getting defaults from generics types are messy
In order to transmit my data I had to initialize each of my records before passing them to the anonymous callback closure. Below is the code to send individual and arrays of records. It is rather elegant:
However, the nastiness was factored out and hidden in TxRec.Default<T>
, which had to figure out type information for type T and then return a default for it. Since generics are generated for each type the code was duplicated for each type that filled T
for class function TxRec.Default<T>: T;
.
I really dislike this code and tried to find alternatives. I investigated the build in function Default(T)
that is used in the Generic Collections to set or return default values, but I could not find a way to override its behavor for my types. The code for Default(T)
is not in system.pas and may be some compiler magic. I finally resolved to leave it as such.
Delphi 10.4 has some new tricks
Unbeknownst to most Delphi developers two new operators were snuck into Delphi 10.4, they are not even in the installed help file, but only mentioned in the online the documentation and in a blog post by Marco Cantu. These two operators are Initialize
and Finalize
and the implications of these are huge (more on this later).
Initialize
allows us to define code that runs when a record first enters into scope, plus and most astounding to me, the code for Initialize
is also called per element in an array if its allocated.
To make our record a “managed” record we add these two operators
Replacing TxRec.Default<T>
with Intialize
operator
In our case we don’t box any types that need to be disposed so we don’t need Finalize
, but we can use Initialize
per each of our records to have them load their size and enumerated type.
For instance, our line record can be changed to look like this:
The rest of the records can similarly be adjusted. This means we no longer have one central function that checks the type information to determine the default value to return, but rather each type controls their own initialization.
Our send code is now even cleaner
Cost and Benefits of Managed records
When we call SetLength and new records are allocated for the array there is one call to Initialize
per element. In my older code it was a faster local memory copy in the loop before configuring the record before transmission.
I don’t know what other overheads are associated with managed records. They seem to be more analogous to C++ classes and structs that exist on the stack instead of dynamically allocated on the free store (like all Delphi objects). Testing would be needed to see if there is any extra processing. In my basic testing I did not notice any differences.
Managed records allow us to have stack managed types that can box types that are dynamically allocated on the free store (colloquially called the heap). Dynamically allocated types can be constructed in Initialize
and disposed in Finalize
without developers needing to call constructors and box code in try-finally blocks. These types are scope managed.
Other Improvements to the Transmitter Class
I realized in rewriting some of the code that signaling that does not require any configuration, or records that can be transmitted once configured based on their own Initialize
code. So there are now three overloads for Send<T>
.
I also considered the need for end-users to control records themselves without a configuration callbacks, and to facilicate that, I added two overloads for SendRecords
Conclusion
The updated repository for this blog series code can be found here. You will notice that its a branch named Delphi10.4, the original source code is still under the master branch
Leave a Comment