OmniThreadLibrary 3.09 has just been released! This is a big release with some new functionality and many bug fixes.
It is available via Git or as a ZIP archive.
random ramblings on Delphi, programming, Delphi programming, and all the rest
OmniThreadLibrary 3.09 has just been released! This is a big release with some new functionality and many bug fixes.
It is available via Git or as a ZIP archive.
Most of the noise about AI and coding is about writing new code. Just check all the examples in the recent Kai release video. Indeed, this is probably the best idea to explain what LLMs can do to a random programmer, but to me the biggest help I get from them is one the other end – not while writing new code but in debugging old, large, sometimes multithreaded Delphi programs. The kind of codebase that no longer fits in one human head, where a crash reproduces once a month on one machine in the field, and the stack trace is forty threads deep.
That’s where Claude Code really shines – as a code investigator tool. This post is a tour of how I use it for debugging, and, just as importantly, how I keep it under control.
I do specifically use Claude Code for all my debugging needs but this article is not tightly coupled to it. Most of the ideas apply the same way to other LLM-based coding tools – Codex, Gemini CLI etc.
┌──────────┬───────┬─────────┬────────┬────────┬─────────────┐
│ Platform │ Found │ Ignored │ Passed │ Failed │ Time │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Win32 │ 322 │ 0 │ 322 │ 0 │ 40 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Win64 │ 322 │ 0 │ 322 │ 0 │ 42 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Linux64 │ 325 │ 3 │ 322 │ 0 │ 32 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Android │ 325 │ 3 │ 322 │ 0 │ 38 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Total │ │ │ 1288 │ 0 │ ~2 min 32 s │
└──────────┴───────┴─────────┴────────┴────────┴─────────────┘
┌──────────┬───────┬─────────┬────────┬────────┬─────────────┐
│ Platform │ Found │ Ignored │ Passed │ Failed │ Time │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Win32 │ 7 │ 0 │ 7 │ 0 │ 293 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Win64 │ 7 │ 0 │ 7 │ 0 │ 293 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Linux64 │ 7 │ 0 │ 7 │ 0 │ 275 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Android │ 7 │ 0 │ 7 │ 0 │ 284 s │
├──────────┼───────┼─────────┼────────┼────────┼─────────────┤
│ Total │ │ │ 28 │ 0 │ ~19 min 5 s │
└──────────┴───────┴─────────┴────────┴────────┴─────────────┘
TL;DR: Here is the documentation and here is the unit.
You probably know I do a lot of multithreaded programming😜 Quite a fun work with lots of fun problems, one of them being sending data across threads. OmniThreadLibrary uses queues and Windows messages for that but sometimes this is just not good enough.
Recently I was solving a problem of dispatching events across multiple threads. I have a UDP receiver that runs in a thread, does some processing (unpacks TS packets from the UDP) and sends unpacked data to receiver threads. I didn't want to use standard OTL messaging mechanisms for that as there are many packets received every second and Windows message queues would surely overflow.
I started working on a custom solution (something with a shared queue where the data is pushed and smart notification mechanism that is not triggered for every UDP packet) but then I got bored and I started thinking about a more general solution. I also wanted to move away from Windows messages as they require special work in the receiver thread. The general internet knowledge pointed me to Asynchronous Procedure Calls and an idea for a new implementation was born!
Delphi 13.1 has exposed a long-present 64-compatibility bug in OmniThreadLibrary which is fixed in this release. Hopefully this was the last 64-compatibility bug in OTL :)
In other news, this release adds a TLightweightMREWEx extension to TLightweightMREW (Delphi 11+ only). Enhanced version supports nested BeginWrite/EndWrite calls. Read more in article Readers-writer lock - Part 4: Improving TLightweightMREW.
There were significant improvements to Locked<T>. On Delphi 11+ it uses TLightweightMREWEx internally (instead of a critical section). New methods (Delphi 11+ only) were added to support locking with read/write semantics: BeginRead, TryBeginRead, EndRead, BeginWrite, TryBeginWrite, EndWrite. On Delphi 11+, Acquire/Release methods map to BeginWrite/EndWrite which preserves existing locking semantics.
Locked<T> also got new access pair Enter/Leave and function IsInitialized. Original access function Acquire is implement as a procedure while new methods BeginRead/BeginWrite work as functions returning original type T. To mirror that, Enter also returns T (while Leave is just an alias for Release).
As a last modifidation, SetThreadName function from OtlCommon.Utils also calls SetThreadDescription Windows API if it is available. This allows logging code to log thread name even if they only know a thread handle by calling GetThreadDescription Windows API.
Download new version from GitHub!
GetIt will be updated soon.
From time to time I got error reports about "error 1400" in OTL and my response is always the same: "You are using it wrong." I never wrote anything about that, though. My bad :(
This time, however, I received an excellent error report with a minimal reproducible example (thanks!), which gave me an excellent excuse for this blog post ;)
Let me start with a little demonstration...
A month ago I wrote a nice little unit that simplifies working with timestamps received from different sources - GpTimestamp - with help from Claude Code. Actually, to be fair - I took a backseat, set the goals, provided some info, and let Claude do all the work. I already wrote a post about GpTimestamp, but I said nothing about the coding experience. Let's fix that!
In my programmer's life I frequently deal with all kinds of timestamps. As the code is relatively, ahem, "mature" those timestamps come from all kinds of sources. There are 64-bit ticks (GetTickCount64), multimedia time (timeGetTime), performance counters (QueryPerformanceCounter), performance counters in disguise (TStopwatch), and there is also a TDateTime (NowUTC) mixed in from time to time.
All this mess would not be that bad had the timestamps been only used locally. If you sample a timestamp and then a few lines later check whether so-and-so time has expired, all is well. You will always know what the source of the timestamp was and what to compare it to.
Problems occur when the timestamps are grabbed on one side of the system and consumed on the other side. Let's say that the code receives a block of data over some internal communication mechanism. A part of that data is a timestamp representing a creation time of that data block. You have to check how much time has elapsed since then. How do you do that?
Well, firstly you have to know in what units this timestamp is stored. Is it a floating point number (TDateTime) or a 64-bit integer? In the latter case, does it store a number of milliseconds, 'ticks' (whatever that is) or something else?
Secondly, you have to get "current timestamp" from the correct time source so you can do the calculation. So you have to know what the source was. And if you're not sure, you'll have to dig through the code to find the point where the original timestamp was created.
The former problem is somewhat solvable by using descriptive variable/field names. For example, if a variable stores a timestamp in milliseconds, I always use a suffix _ms. In the example above I would use a field DataCreationTime_ms or something similar.
The latter problem, however, is hard. You could use suffixes (DataCreationTime_Tick64_ms) but that gets ugly pretty fast. You could also document the time source (and you should!) but that again stops you when you just want to write some code.
Even worse, both solutions only work if you take care when coding. Delphi won't stop you from subtracting TStopwatch.GetTimeStamp from a value that is storing a result of timeGetTime, although the result won't make much sense.
Clearly, I needed something better, so I wrote* the GpTimestamp unit which tries to solve this mess. There is also documentation and unit tests, if you want to jump in now.
(* When I say "I" I actually mean Claude. I was just an idea-man on this project. I'll say more about that in the following post...)
Remember the good old times when strings were composed of 8-bit characters? Those were simpler times. When we needed a data buffer, we just used an AnsiString. An 8-bit character is just a byte, surely?
Then Unicode arrived and suddenly all strings were based on 16-bit characters. The code broke all around us. At the company I'm working for we needed about one year to port all our applications to Unicode. (And by “we” I mean myself. I allocated one day per week to work on Unicode issues and slowly worked through the code. Today I'd probably use AI for the task. :) )
We completed this port in Delphi 2010 times. And then we spent the next 10-ish Delphi versions fixing bugs related to this change. And when I thought we had fixed them all (hope, hope), Microsoft did something unexpected.
While I don't miss the Refactor menu in Delphi 13 -- I'm used to MMX -- I do sometimes miss ability to format Delphi code (the in/famous Ctrl+D). On my recent RAD Studio presentation in Ljubljana I learned that there are many other Delphi programmers who also miss this functionality, so I looked around to find a way of restoring it.
As it happened, I didn't have to look far. Pretty quick I found an excellent blog post written by David Cornelius. If you also miss code formatter, read his post!
I just released a new OmniThreadLibrary. Not many changes, just some fixes and - of course - support for RAD Studio 13.
OmniThreadLibrary now supports 19 major Delphi releases: 2007, 2009, 2010, XE, XE2, XE3, XE4, XE5, XE6, XE7, XE8, 10 Seattle, 10.1 Berlin, 10.2 Tokyo, 10.3 Rio, 10.4 Sydney, 11 Alexandria, 12 Athens, and 13 Florence. I wonder whether this is a record in the Delphi open-source world :)
New features
Next week I'm presenting all that is shiny and new in RAD Studio 13 in Ljubljana. As the presentation will be in Slovenian language, so is the next of this post ...
Some time ago, I tried using AI to write a blog post, but things didn’t go quite as planned. For my second attempt, I aimed to do better. I focused on a single engine—OpenAI—and used it in a more granular way. Instead of simply saying, “Write me an article about...,” I started with basic introductory questions, then asked it to write the article, and finally requested improvements where I felt the draft was lacking. The end result was quite satisfying.
This article is my analysis of that process. If you’re only interested in reading the transcript, you can find it here.
But the real changes happened under the hood.
I’ve refactored the codebase to fully separate the AI client logic from both the UI and the network layer. And what does that mean for you? It means you can now use the Chatterbox AI libraries directly in your own code.
Let me say that again, loud and clear:
You can now use Chatterbox AI libraries from your code!
To test this, I created a very basic demo called EngineDemo. And naturally, for this announcement, I had to explain how the demo uses the Chatterbox libraries.
Since this was a brand-new development that the AI had no prior knowledge of, I decided to revisit an experiment I mentioned in a previous post—one that was only partially successful. This time, I took a different approach. But more on that in the next post.
For now, take a look at what ChatGPT had to say on the topic. Just keep in mind: this wasn’t a simple “click and publish” process. ChatGPT and I had a long back-and-forth, and the result of that collaboration is the article below.
This was supposed to be an article comparing three major AI players in their ability to write technical articles. In preparation, however, I made some mistakes ... :(
When I prepared for the initial article, I asked them to fetch the data from https://github.com/gabr42/ComputeCore/blob/main/ComputeCore.pas and write about it. (See the end of this article for the initial query and results.) At that moment, however, that link was not public yet. All three AIs ignored that and happily wrote about the code they knew nothing about. It was only Claude that complained about the bad link but I missed that while checking the results.
So I decided to rerun the experiment on all platforms. I started by asking the engines the following question:
Can you accesss https://github.com/gabr42/ComputeCore/blob/main/ComputeCore.pas and provide an overview of the code in one paragraph?
Can you accesss https://raw.githubusercontent.com/gabr42/ComputeCore/refs/heads/main/ComputeCore.pas and provide an overview of the code in one paragraph?
Analyzing the answer shows that the answers were not done purely on the basis of the linked code. Both Claude and Gemini have included details that they got from my previous article on ComputeCode.
In the answers below I marked hallucinated misinformation with red and information that was obviously retreived from different sources with blue.
Today, I want to talk about a small multithreading kernel (framework, library, or whatever you want to call it) that I've recently helped develop for the Spring4D project.
ComputeCore is a lightweight parallel processing framework for Delphi that lets you easily run CPU-intensive tasks across multiple threads. Its goals are straightforward: to run (possibly compute-heavy) tasks concurrently, allow tasks to spawn child tasks, and expose a very simple interface for the caller.
It will eventually be published as Spring.Threading, but until that happens, we can examine the almost-final version at https://github.com/gabr42/ComputeCore. In Spring4D, it will run the parallel implementation of the new sorting algorithm, but that doesn't mean it is only useful for one specific job! It is indeed a completely open (although intentionally simple) framework that can be used for your projects too.
[This article was written with the help of an AI. My next article will explain this decision and the writing process.]
Next Wednesday I'll be talking about Delphi and TMS Web Core in Ljubljana. As the presentation will be in Slovenian language, so is the rest of this post.
Due to potential privacy concerns with DeepSeek servers (we’re unsure if the data sent over the paid API is kept private), I looked into some smaller DeepSeek models available on the Ollama.com site. These models use less complex AI with fewer parameters than the online version, but they might still be good enough for an average Delphi programmer. We’ll see.
For testing, I used a powerful RTX 4090 card with 24 GB of memory. If your graphics card has less memory, your selection of useful models will be more limited.
I asked all models the same two questions: one on a general programming topic and another specific to the FireMonkey platform. The first question was:
"I have a multiline string containing newline ASCII characters (TMemo.Text). I want to change it to a single-line string with only printable ASCII characters. I could do that with BASE64 encoding, for example. I would, however, like to keep the text as much readable as possible by "encoding" only non-printable characters. Is there a simple way to do that?"
You can check Codellama’s response in an older post (Codellama being the only local model I had tested so far): Delphi and AI [5]: Encoding Multi-line Strings.
Last week, I asked the same question to the online DeepSeek-Reasoning model. Check the answers in this post: Delphi and AI [6]: DeepSeek-Reasoning Model.
The second question was:
"How can I copy text to clipboard in a Delphi Firemonkey application?"
Read here for Codellama’s response: Delphi and AI [4]: Device-ndependent clipboard.
The answer from the online DeepSeek-Reasoning model can be found at the end of this post.
Let’s see how the models are performing! As always, full logs are available on GitHub.
As I’m writing this, a new Chatterbox release is available on GitHub. As with the initial release, you can either download the source and recompile it or grab an executable (Windows, 32-bit) from the release.
(If you have never heard of Chatterbox, you should probably read the initial release article.)
The biggest change since v0.1 is support for DeepSeek AI.
An interesting AI has just appeared on my radar - DeepSeek. It exposes a "normal" chat model deepseek-chat and a "reasoning" (that's the interesting part) model deepseek-reasoning. While working on DeepSeek support for the Chatterbox (more on that in few days) I thought it would be interesting to ask the "reasoning" model the "multi-line string encoding" question that every other AI has failed.
Warning: DeepSeek privacy policy states: "When you use our Services, we may collect your text or audio input, prompt, uploaded files, feedback, chat history, or other content that you provide to our model and Services." Don't send any private or proprietary information in the chat!
The log is provided on GitHub.
Just as a reminder, the question was:
"I have a multiline string containing newline ASCII characters (TMemo.Text). I want to change it to a single-line string with only printable ASCII characters. I could do that with BASE64 encoding, for example. I would, however, like to keep the text as much readable as possible by "encoding" only non-printable characters. Is there a simple way to do that?"
Recently I had to convert a multi-line string into a single-line string value for storage (ignore the 'why' of it; let's just blame it on a legacy code). My first idea was to do a Base64 encode of the string, but I was in a mood for some fun and so I asked my friendly AI helpers:
"I have a multiline string containing newline ASCII characters (TMemo.Text). I want to change it to a single-line string with only printable ASCII characters. I could do that with BASE64 encoding, for example. I would, however, like to keep the text as much readable as possible by "encoding" only non-printable characters. Is there a simple way to do that?"
Let's see what they came up with!
Full transcripts, as usual, are on GitHub. This time I had also created a program containing all implementations.
While working on Chatterbox I ran into number of problems, most of them caused by my limited knowledge of device-independent programming with Firemonkey. One particular challenge was copying data to the clipboard - something that is AFAIK available on all supported platforms. So I asked my friendly AI helpers:
"How can I copy text to clipboard in a Delphi Firemonkey application?"As always, all logs are available on GitHub.
It's the time of the year to be merry and have fun, so no code today!
I've decided to ask our future AI overlords about the meaning of Christmas. The Delphi way, of course!
Today I'm posting just the answer I liked the most. You can read the others on GitHub.
Happy holidays, fellow programmers! And remember - Delphi, it is the way!
![]() |
| As befits an article on AI-generated content, the image was created by Midjourney. |
t’s great that Embarcadero has added an AI Chat window to RAD Studio, but let’s be honest—the implementation is lacking. As one participant at a recent workshop noted: “It looks like someone wrote it two hours before the release.” Sadly, I have to agree. The AI Chat feature is practically useless if you intend to use AI for anything more than a quick demo.
That’s when I started considering better ways to interface with LLMs. Instead of searching for an alternative, I decided to build my own AI chat interface. It’s called Chatterbox, and it’s fully open source. You can find it on GitHub under a “do with it (almost) whatever you want” license.
To start using Chatterbox, you can either build it from source or download the precompiled EXE from GitHub (currently available only for Windows 32-bit). The app is written with FireMonkey, so—at least theoretically—it can also be built for macOS, iOS, Android, and Linux. However, I haven’t tested it on platforms other than Windows.
To build Chatterbox, you’ll need the following libraries: Spring4D, DCPCrypt2, and TAES. Links to these dependencies are included in the README file.
While preparing for my Delphi and AI workshop, I decided to keep a log of all my interactions with AI helpers in a file for later analysis. Initially, I searched for an existing utility to log clipboard changes to a file (and I found one), but then I thought—why not ask the AI helpers to help me create one? After all, it’s not a big problem: set up a timer, check if the clipboard content changes, and log the content to a file. What could be simpler?
I posed the same question to all five engines:
"I want to create a Delphi application that would monitor clipboard content (on Windows) and append clipboard content to a log file each time the clipboard has changed (and has a text inside)."
Let's see how they performed!
Logs and code are available here.
Recently, I led a workshop in Slovenia where we explored the current state of AI in relation to Delphi programming. (A note to participants: the slides are finally online—apologies for the delay!) The initial results were, let’s say, interesting enough to warrant further study.
Now, let’s see how today’s "state-of-the-art" AIs perform with Delphi programming!
The contenders are:
This is an invitation to the workshop in Ljubljana next week. As the session will be in Slovenian language, so is this invitation ...
|
Next Wednesday, 10th, I'll be talking about Delphi and Pyhon in Ljubljana. As usual for Slovenian workshops, the talk will be in Slovenian language so I'll continue this invitation appropriately ...
|
|
Next week I'll present all about the new RAD Studio 12 in Ljubljana. As usual, the presentation will be in Slovenian language and so will be the rest of this post ...
It is so nice when you see how a small idea grows into a nice, rounded project!
Years age I wrote a unit that allowed you to write SQL statements as Pascal code (GpSQLBuilder). This has allowed me to write a code like this:
query := CreateGpSQLBuilder;
query
.Select.All
.From(DB_TEST)
.OrderBy(
query.&Case
.When([COL_2, '< 0']).&Then(COL_3)
.&Else(COL_4)
.&End);It was a small project with minimum support -- as long as it generated SQL code that I've needed, I was fine with it. Much of the SQL language support was missing, there was no support for different SQL dialects and so on ...
Luckilly, Isaque Pinheiro liked the idea and converted it into a full-fledged library with support for multiple SQL dialects, much more complete SQL language support, units tests, installer, a ton of samples and more.
The second edition of my book Delphi High Performance is now released! Get all 452 pages of Delphi goodness with two new chapters and all the updated and improved content at Amazon or at Packt Publishing!