Kamis, 23 April 2026

Building New PC To Study LLM

Since early 2025, I've been using AI LLMs like ChatGPT or DeepSeek to learn programming or help with research. Using AI LLMs is easier because they can collect and process data into information that's easier for me to understand.

 

At the time, I didn't know that AI LLMs could be run on a local computer. After reading several articles about SLMs (Small Language Models) that can be run on a local computer, I became interested in running AI LLMs on my computer.

For the software, I tried using Llama.cpp, which has acceleration support for Vulkan.

Initially, I used the computer I usually use for gaming in my spare time. The specifications are as follows:

  • Motherboard: Intel H81
  • Processor: Intel Core i5 4670 3.4-3.7 GHz Quad Core.
  • RAM: 16 GB DDR3 1600 MHz.
  • GPU: Nvidia GeForce GTX 1050 4 GB DDR5.
  • OS: Windows 10
  • SSD: 256 GB.
  • Hard Disk: 1 TB.

With the above specifications, I successfully ran the LLM model with 4B and 7B parameters, but it would crash if the parameters were set higher. For 4B parameters, with CPU-only, it produced 5 tokens/s, while with Vulkan acceleration, it could reach 11 tokens/s. Meanwhile, for LLM, with 7B parameters, with CPU-only, it produced 3 tokens/s, while with Vulkan acceleration, it could reach 8 tokens/s.

From the measurement results above, I concluded that the bottleneck was the DDR3 RAM speed and the GPU's RAM size, which was too small. Therefore, LLM with parameters of 7B or higher would use more RAM.

 

To address the above issues, I finally allocated the budget to upgrade my computer to:

  • Motherboard: MSI A520.
  • Processor: AMD Ryzen 5 3400G 3.6-4.2 GHz Quad Core with hyperthreading.
  • RAM: 64 GB DDR4 3200MHz.
  • GPU: AMD Vega 11 integrated 2 GB shared RAM.
  • OS: Windows 10.
  • SSD: 256 GB.
  • Hard Disk: 1 TB.

The iGPU is able to use all available RAM and not limited by shared RAM setting in the BIOS. On Windows computers, when 50% of RAM is used, it will start aggressively swapping RAM, making the computer very sluggish. On Linux, this parameter can be changed, for example, starting when there's 10% RAM remaining. Since I have a relatively large amount of RAM, I intentionally disabled the swap file so I could use more than 50% of RAM for running LLM. The maximum stable RAM speed is at 3200Mhz even though the RAM specification is 3600 MHz, possibly due to a defect in the RAM due to overclocking by previous owner or limitations of the Ryzen 3400G's memory controller. Luckily it can run very stable at 3200Mhz without any problems.

With these new specifications, I was able to run LLM models with 35B and even 80B parameters with LLama.cpp in Vulkan mode. On this computer, Qwen 3.0 Coder Next 80B A3B could run at 10 tokens/s with iGPU. LLama.cpp with Vulkan mode is more power efficient and quiter than the CPU only mode.

Beside running LLM this computer is good for casual gaming too and only consuming 24 watt at idle. The AMD Radeon GPU driver for Windows have great compatibility with games. The Age of Empires HD Edition is broken when viewing civilization's tech tree with my GTX 1050 but run flawlessly with the Ryzen 3400G iGPU.

Kamis, 22 Januari 2026

Searching Algorithms That I Often Use


As a Computer Science student I have learn a lot of algorithm that not only usable in programming but also in my daily life. For example, I save alot of time when using binary search for searching in ordered objects.

1. Hash Map

Hash Map using integer of hash function result of item's key to map object into bucket array. The object is mapped using modulo of hash value and bucket size as index in the bucket array. And if there is collision the item is added using linked list in the collided index. The item search is doing by calculate hash value of search key and using modulo to find index in the bucket array. And if index in bucket is occupied then continue to compare item's key with search key, if not equal then continue with items in the linked list until the key is equal, if all items is not equal then item is not found.

Using Hash Map is very fast for alot of items and is frequently searched but when new item is added and the bucket need to grow it need to rehash the entire bucket into new bucket.
 
This is an example of my Hash Map classes in Free Pascal: 
  1. type 
  2.   IListIterator = interface
  3.     ['{16585733-6438-4D58-A772-FC6811EB19BB}']
  4.     procedure First;
  5.     function Next: TObject;
  6.     procedure Delete;
  7.   end;
  8.  
  9.   { THashable }
  10.   THashable = class(TObject)
  11.   private
  12.     FNext__: THashable;
  13.   protected
  14.     FHash: Integer;
  15.   public
  16.     constructor Create;
  17.     function IsEqual(AKey: Pointer): Boolean; virtual;
  18.     property Next__: THashable read FNext__;
  19.     property Hash: Integer read FHash;
  20.   end;
  21.  
  22.   { THashMap }
  23.   THashMap = class(TObject)
  24.   private
  25.     FBucket: PObjectArray;
  26.     FCapacity: integer;
  27.     FCount: Integer;
  28.     procedure SetCapacity(NewCapacity: Integer);
  29.   public
  30.     function ObjectByKey(AKey: Pointer): THashable;
  31.   protected
  32.     procedure Grow; virtual;
  33.     function HashKey(AKey: Pointer): Integer; virtual;
  34.     property Capacity: integer read FCapacity write SetCapacity;
  35.     property Count: Integer read FCount;
  36.   public
  37.     destructor Destroy; override;
  38.     procedure Clear;
  39.     function GetObject(AKey: Pointer): THashable;
  40.     procedure Put(AItem: THashable);
  41.     procedure Remove(const AKey: Pointer);
  42.     function GetIterator: IListIterator;
  43.   end;
  44.  
  45.   { THashMapIterator }
  46.   THashMapIterator = class(TInterfacedObject, IListIterator)
  47.   private
  48.     FHashMap: THashMap;
  49.     FIndex: Integer;
  50.     FNode, FBefore: THashable;
  51.     FReread: Boolean;
  52.   public
  53.     constructor Create(AHashMap: THashMap);
  54.     procedure First;
  55.     function Next: TObject;
  56.     procedure Delete;
  57.   end;
  58.  
  59. { THashable }
  60. constructor THashable.Create;
  61. begin
  62.   FHash := 0;
  63.   FNext__ := nil;
  64. end;
  65.  
  66. function THashable.IsEqual(AKey: Pointer): Boolean;
  67. begin
  68.   Result := False;
  69. end;
  70.  
  71. { THashMap }
  72. procedure THashMap.Clear;
  73. var
  74.   I: integer;
  75. begin
  76.   if (FBucket <> nil) then
  77.   begin
  78.     for I := 0 to FCapacity - 1 do
  79.     begin
  80.       if (FBucket^[I] <> nil) then FreeAndNil(FBucket^[I]);
  81.     end;
  82.     FreeMem(FBucket);
  83.     FBucket := nil;
  84.   end;
  85.   FCapacity := 0;
  86.   FCount := 0;
  87. end;
  88.  
  89. destructor THashMap.Destroy;
  90. begin
  91.   Clear;
  92.   inherited;
  93. end;
  94.  
  95. function THashMap.GetObject(AKey: Pointer): THashable;
  96. var
  97.   P: THashable;
  98.   H: integer;
  99. begin
  100.   if (FCount > 0) then
  101.   begin
  102.     H := HashKey(AKey);
  103.     P := THashable(FBucket^[H mod FCapacity]);
  104.     while ((P <> nil) and ((P.Hash <> H) or not P.IsEqual(AKey))) do
  105.       P := P.FNext__;
  106.     Result := P;
  107.   end
  108.   else
  109.     Result := nil;
  110. end;
  111.  
  112. procedure THashMap.Grow;
  113. var
  114.   Delta: Integer;
  115. begin
  116.   if FCapacity > 64 then
  117.     Delta := FCapacity div 4
  118.   else if FCapacity > 8 then
  119.     Delta := 16
  120.   else
  121.     Delta := 4;
  122.   SetCapacity(FCapacity + Delta);
  123. end;
  124.  
  125. function THashMap.HashKey(AKey: Pointer): Integer;
  126. begin
  127.   Result := Integer(AKey);
  128. end;
  129.  
  130. function THashMap.ObjectByKey(AKey: Pointer): THashable;
  131. begin
  132.   Result := GetObject(AKey);
  133.   if (Result = nil) then
  134.     raise EListError.CreateFmt(SListItemNotFoundError,[IntToHex(Integer(AKey),8)]);
  135. end;
  136.  
  137. procedure THashMap.Put(AItem: THashable);
  138. var
  139.   I: integer;
  140. begin
  141.   if (AItem = nil) then exit;
  142.   Inc(FCount);
  143.   if ((FCount * 4) div 3 > FCapacity) then Grow;
  144.   I := AItem.Hash mod FCapacity;
  145.   AItem.FNext__ := THashable(FBucket^[I]);
  146.   FBucket^[I] := AItem;
  147. end;
  148.  
  149. procedure THashMap.Remove(const AKey: Pointer);
  150. var
  151.   P, Q: THashable;
  152.   I, H: Integer;
  153. begin
  154.   if (FCount > 0) then
  155.   begin
  156.     H := HashKey(AKey);
  157.     I := H mod FCapacity;
  158.     P := THashable(FBucket^[I]);
  159.     Q := nil;
  160.     while ((P <> nil) and (P.Hash <> H) and not P.IsEqual(AKey)) do
  161.     begin
  162.       Q := P;
  163.       P := P.FNext__;
  164.     end;
  165.     if (P <> nil) then
  166.     begin
  167.       if (Q = nil) then
  168.         FBucket^[I] := P.FNext__
  169.       else
  170.         Q.FNext__ := P.FNext__;
  171.       P.Free;
  172.       Dec(FCount);
  173.     end;
  174.   end;
  175. end;
  176.  
  177. function THashMap.GetIterator: IListIterator;
  178. begin
  179.   Result := THashMapIterator.Create(Self);
  180. end;
  181.  
  182. procedure THashMap.SetCapacity(NewCapacity: Integer);
  183. var
  184.   P, Q: THashable;
  185.   NewList: PObjectArray;
  186.   I, J: Integer;
  187. begin
  188.   if ((NewCapacity = FCapacity) or (NewCapacity < FCount)) then exit;
  189.   if (NewCapacity > 0) then
  190.   begin
  191.     GetMem(NewList,NewCapacity*SizeOf(TObject));
  192.     FillChar(NewList^,NewCapacity*SizeOf(TObject),0);
  193.     for I := 0 to FCapacity - 1 do
  194.     begin
  195.       P := THashable(FBucket^[I]);
  196.       while (P <> nil) do
  197.       begin
  198.         Q := P;
  199.         P := P.FNext__;
  200.         J := Q.Hash mod NewCapacity;
  201.         Q.FNext__ := THashable(NewList^[J]);
  202.         NewList^[J] := Q;
  203.       end;
  204.     end;
  205.   end
  206.   else
  207.     NewList := nil;
  208.   if (FBucket <> nil) then FreeMem(FBucket);
  209.   FBucket := NewList;
  210.   FCapacity := NewCapacity;
  211. end;
  212.  
  213. { THashMapIterator }
  214. constructor THashMapIterator.Create(AHashMap: THashMap);
  215. begin
  216.   FHashMap := AHashMap;
  217.   FIndex := -1;
  218.   FNode := nil;
  219.   FBefore := nil;
  220.   FReread := False;
  221. end;
  222.  
  223. procedure THashMapIterator.First;
  224. begin
  225.   FIndex := -1;
  226.   FNode := nil;
  227.   FBefore := nil;
  228.   FReread := False;
  229. end;
  230.  
  231. function THashMapIterator.Next: TObject;
  232. begin
  233.   if (FReread) then
  234.   begin
  235.     FReread := False;
  236.   end
  237.   else begin
  238.     if (FNode <> nil) then
  239.     begin
  240.       FBefore := FNode;
  241.       FNode := FNode.FNext__;
  242.     end;
  243.     if (FNode = nil) then
  244.     begin
  245.       Inc(FIndex);
  246.       while ((FIndex < FHashMap.Capacity) and (FHashMap.FBucket^[FIndex] = nil)) do
  247.         Inc(FIndex);
  248.       if (FIndex < FHashMap.Capacity) then
  249.         FNode := THashable(FHashMap.FBucket^[FIndex])
  250.       else
  251.         FNode := nil;
  252.       FBefore := nil;
  253.     end;
  254.   end;
  255.   Result := FNode;
  256. end;
  257.  
  258. procedure THashMapIterator.Delete;
  259. var
  260.   P: THashable;
  261. begin
  262.   if (FNode <> nil) then
  263.   begin
  264.     P := FNode;
  265.     if (FBefore <> nil) then
  266.       FBefore.FNext__ := P.FNext__
  267.     else
  268.       FHashMap.FBucket^[FIndex] := P.FNext__;
  269.     FNode := FNode.FNext__;
  270.     if (FNode = nil) then
  271.     begin
  272.       Inc(FIndex);
  273.       while ((FIndex < FHashMap.Capacity) and (FHashMap.FBucket^[FIndex] = nil)) do
  274.         Inc(FIndex);
  275.       if (FIndex < FHashMap.Capacity) then
  276.         FNode := THashable(FHashMap.FBucket^[FIndex])
  277.       else
  278.         FNode := nil;
  279.       FBefore := nil;
  280.     end;
  281.     FReread := True;
  282.     P.Free;
  283.   end;
  284. end;
  285.  
  286. { TNamedObject }
  287. function TNamedObject.GetName: String;
  288. begin
  289.   Result := FName;
  290. end;
  291.  
  292. procedure TNamedObject.SetName(const Value: string);
  293. begin
  294.   FName := Value;
  295.   FHash := HashName(Value);
  296. end;
  297.  
  298. function TNamedObject.IsEqual(AKey: Pointer): Boolean;
  299. begin
  300.   Result := SameText(FName, String(AKey));
  301. end;
 
This is an example of Hash function used to map object using case insensitive name: 
 
  1. function HashName(const AName: string): integer;
  2. var
  3.   I: integer;
  4.   C: Byte;
  5. begin
  6.   Result := 0;
  7.   for I := 1 to Length(AName) do
  8.   begin
  9.     C := Byte(AName[I]);
  10.     if ((C >= Ord('a')) and (C <= Ord('z'))) then Dec(C,Ord('a')-Ord('A'));
  11.     Result := ((Result SHL 5) OR (Result AND $1F)) + C;
  12.   end;
  13.   Result := Result AND $7FFFFFFF;
  14. end;
This an example of using the Hash Map class to map objects by case insensitive name:
  1. type  
  2.   { TNamedObject }
  3.   TNamedObject = class(THashable)
  4.   private
  5.     FName: string;
  6.   protected
  7.     function GetName: String;
  8.     procedure SetName(const Value: string); virtual;
  9.   public
  10.     function IsEqual(AKey: Pointer): Boolean; override;
  11.     property Name: string read GetName write SetName;
  12.   end; 
  13.  
  14.   { TNamedObjectMap }
  15.   TNamedObjectMap = class(THashMap)
  16.   protected
  17.     function HashKey(AKey: Pointer): Integer; override;
  18.   public
  19.     function GetObject(const AName: String): TNamedObject;
  20.     procedure Remove(const AName: string);
  21.   end;
  22.  
  23.  { TNamedObject }
  24. function TNamedObject.GetName: String;
  25. begin
  26.   Result := FName;
  27. end;
  28.  
  29. procedure TNamedObject.SetName(const Value: string);
  30. begin
  31.   FName := Value;
  32.   FHash := HashName(Value);
  33. end;
  34.  
  35. function TNamedObject.IsEqual(AKey: Pointer): Boolean;
  36. begin
  37.   Result := SameText(FName, String(AKey));
  38. end;
  39.  
  40. { TNamedObjectMap }
  41. function TNamedObjectMap.HashKey(AKey: Pointer): Integer;
  42. begin
  43.   Result := HashName(String(AKey));
  44. end;
  45.  
  46. function TNamedObjectMap.GetObject(const AName: String): TNamedObject;
  47. begin
  48.   Result := TNamedObject(inherited GetObject(Pointer(AName)));
  49. end;
  50.  
  51. procedure TNamedObjectMap.Remove(const AName: string);
  52. begin
  53.   inherited Remove(Pointer(AName));
  54. end;

This is an example of name and row index search helper using Hash Map:

  1.   { TNameIndexItem }
  2.   TNameIndexItem = class(THashable)
  3.   private
  4.     FKey: String;
  5.     FRow: Integer;
  6.     FData: Pointer;
  7.   public
  8.     constructor Create(const AKey: String; ARow: Integer; AData: Pointer=nil);
  9.     function IsEqual(AKey: Pointer): Boolean; override;
  10.     property Key: String read FKey;
  11.     property Row: Integer read FRow;
  12.     property Data: Pointer read FData;
  13.   end;
  14.  
  15.   { TNameIndex }
  16.   TNameIndex = class(THashMap)
  17.   protected
  18.     function HashKey(AKey: Pointer): Integer; override;
  19.   public
  20.     function GetObject(const AKey: String): TNameIndexItem;
  21.     procedure Remove(const AKey: String);
  22.     procedure PutRow(const AKey: String; ARow: Integer; AData: Pointer=nil);
  23.     function FindRow(const AKey: String): Integer;
  24.     function GetRowAndData(const AKey: String; var ARow: Integer;
  25.       var AData: Pointer): Boolean;
  26. //    procedure IndexTable(ATable: ITransportTable; const AColName: String);
  27.   end;
  28.  
  29. { TNameIndexItem }
  30. constructor TNameIndexItem.Create(const AKey: String; ARow: Integer;
  31.   AData: Pointer);
  32. begin
  33.   FKey := AKey;
  34.   FHash := HashName(AKey);
  35.   FRow := ARow;
  36.   FData := AData;
  37. end;
  38.  
  39. function TNameIndexItem.IsEqual(AKey: Pointer): Boolean;
  40. begin
  41.   Result := SameText(FKey, String(AKey));
  42. end;
  43.  
  44. { TNameIndex }
  45. function TNameIndex.FindRow(const AKey: String): Integer;
  46. var
  47.   P: TNameIndexItem;
  48. begin
  49.   P := GetObject(AKey);
  50.   if (P <> nil) then
  51.     Result := P.Row
  52.   else
  53.     Result := -1;
  54. end;
  55.  
  56. function TNameIndex.GetRowAndData(const AKey: String; var ARow: Integer;
  57.   var AData: Pointer): Boolean;
  58. var
  59.   P: TNameIndexItem;
  60. begin
  61.   P := GetObject(AKey);
  62.   if (P <> nil) then
  63.   begin
  64.     ARow := P.Row;
  65.     AData := P.Data;
  66.     Result := True;
  67.   end
  68.   else
  69.     Result := False;
  70. end;
  71.  
  72. function TNameIndex.GetObject(const AKey: String): TNameIndexItem;
  73. begin
  74.   Result := TNameIndexItem(inherited GetObject(Pointer(AKey)));
  75. end;
  76.  
  77. function TNameIndex.HashKey(AKey: Pointer): Integer;
  78. begin
  79.   Result := HashName(String(AKey));
  80. end;
  81.  
  82. //procedure TNameIndex.IndexTable(ATable: ITransportTable;
  83. //  const AColName: String);
  84. //var
  85. //  i, col: Integer;
  86. //begin
  87. //  if (ATable = nil) then
  88. //    exit;
  89. //  col := ATable.FindField(AColName);
  90. //  if (col < 0) then
  91. //    exit;
  92. //  Clear;
  93. //  for i := 0 to ATable.RowCount - 1 do
  94. //    PutRow(ATable.Cells[col,i].AsString, i);
  95. //end;
  96.  
  97. procedure TNameIndex.PutRow(const AKey: String; ARow: Integer;
  98.   AData: Pointer);
  99. begin
  100.   if (GetObject(AKey) = nil) then
  101.   begin
  102.     Put(TNameIndexItem.Create(AKey, ARow, AData));
  103.   end;
  104. end;
  105.  
  106. procedure TNameIndex.Remove(const AKey: String);
  107. begin
  108.   inherited Remove(Pointer(AKey));
  109. end;

2. Hash Marking

Hash marking using integer of hash function result of item's key to mark list items. Because comparing an integer only need 1 cpu clock and comparing text need 1 cpu clock per character we can skip comparing entire key by first comparing only hash values and only if it is equal then we continue to compare the key.

This method is very effective if item's key is a string or large complex type such as struct and number of items is limited or frequently recreated. For very alot of items and is frequently searched using Hash Map is faster.

This is an example of using hash marking to search for an object in a linked list:
  1. function TChain.GetObject(AKey: Pointer): THashable;
  2. var
  3.   P: THashable;
  4.   H: Integer;
  5. begin
  6.   P := FFirst;
  7.   H := HashKey(AKey);
  8.   while ((P <> nil) and ((P.Hash <> H) or not P.IsEqual(AKey))) do
  9.     P := P.FNext__;
  10.   Result := P;
  11. end;

3. Binary Search

Binary Search work with ordered list by repeatly search the middle first and if not the equal then search the half part of the list that is possibly still cotains the items until the item is found or range start > range end.

This is an example of searching data using binary search:
  1. function TIntegerList.IndexOfB(AItem: Integer): Integer;
  2. var
  3.   Low,High,Mid: Integer;
  4. begin
  5.   Result := -1;
  6.   Low := 0;
  7.   High := Count - 1;
  8.   Mid := (Low + High) div 2;
  9.   while (Low <= High) do
  10.   begin
  11.     if (Items[Mid] > AItem) then High := Mid - 1
  12.     else if (Items[Mid] < AItem) then Low := Mid + 1
  13.     else begin
  14.       Result := Mid;
  15.       exit;
  16.     end;
  17.     Mid := (Low + High) div 2;
  18.   end;
  19. end;

Selasa, 16 Desember 2025

Creating Expert System for Database Application Development

In the year 2016, my friend show me he create a desktop application using Delphi that he create very fast. I think I cannot compete with him with that software development skill. I am thinking what if I create an expert system to help create desktop application for me. Then I create this application and name it Gampang Builder.

The design of Gampang Builder is very simple, it get input from the user about the application design and generate software project for it, not a complete software but only:

  1. Main form and main menu of the application.
  2. Navigation system to access facilities in the application and limiting access by application user. 
  3. Data service for create, edit and delete for entities and relations.
  4. Editing form for the entites and relations.
  5. Printable report for the entities.

 

When we create an application first we have to define the backgroud of the problem to solve and the target to be achieved by the application.

Then we choose the technology for the to be generated application.

To make our code more reusable I split the code into few modules.

In each module we can define the class name for the data service and access right than can be used to limit user access because not every user will have the same access level.

After we define the modules we can define the entities by the data table and relations between entities in the applications.

In the data tabel we can define field name, data type, its characteristic, and relation with other table such as look up or master-detail.


If we need the printed design for documentation, Gampang Builder can print it.

The last thing to do is generate the application but because this application is too old and many changes to the library change the package file reader, I will repair the code and update when it completed. I created a few application with this program, one of the application is a medical laboratory database application and is still running today from 2016. I dont have time yet to improve this program but maybe someday.

UPDATE: Yesterday I have a requirement to generate a flutter Application so I have to fix the Gampang Builder before adding flutter application code generation module. So I open this project again and fix the problem with my modified library. And the problem is TTransportVar object using different format when storing String data. After I fix the file packer object with the modified library then everything is working again and the code generation is back to normal. 

This is the generated folder for the middle ware of an Lazarus Application:

 

And this is the generated folder for the client app:


 This is one of the server module API code:

 

This is one of the client save edit procedure:


 It is very simple but the generated code compiled without any errors and easy to add code later.

That's all in this article, I hope it's usefull.

Creating Simple Web Server with Lazarus

 

Web server is a program or sub program that provide data to client program not limited to web browser using HTTP or HTTPS (HTTP over SSL) protocol. HTTP is a simple, fast and robust protocol for client-server application. The HTTP protocol works by having the client send a request to the server, then the server processes the request and sends back a response to the client.

Example of HTTP Client's Request:

GET / HTTP/1.1 Host: www.example.com

The first line of a HTTP request consist of a command, a url and the protocol version. Followed by header lines and terminated by empty line. Each header line consist of header name and value separated by colon and one space. After the header data is the content data that contain n bytes where n is the value of Content-Length header.

Example of HTTP Server's Response:

HTTP/1.1 200 OK Date: Mon, 23 May 2005 22:38:34 GMT Content-Type: text/html; charset=UTF-8 Content-Length: 155 Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) ETag: "3f80f-1b6-3e1cb03b" Accept-Ranges: bytes Connection: close <html> <head> <title>An Example Page</title> </head> <body> <p>Hello World, this is a very simple HTML document.</p> </body> </html>

The first line of a HTTP response consist of protocol version and result code followed by error message. Followed by header lines and terminated by empty line. Each header line consist of header name and value separated by colon and one space. After the header data is the content data that contain n bytes where n is the value of Content-Length header.

Free Pascal, Delphi and Lazarus share the same programming language the Object Pascal with little differentiation. So many Delphi component can be used in Lazarus and vice versa because it only requires minor changes in the source code to support both programming. If you don't want to use visual component you can use operating system API like winsock (Windows Socket) for Windows or unisock (Unix Socket) for Linux. I will use synapse for Free Pascal library in this example.

To help parsing input data I create TStringSplitter :

  1.      type
  2.       { TStringSplitter }
  3.       TStringSplitter = class(TObject)
  4.       private
  5.         FLine: String;
  6.         FCol: Integer;
  7.         procedure SetCol(AValue: Integer);
  8.         procedure SetLine(AValue: String);
  9.       public
  10.         constructor Create;
  11.         function Fetch(const ATerminator: String=''): String;
  12.         property Line: String read FLine write SetLine;
  13.         property Col: Integer read FCol write SetCol;
  14.       end;

  15.     function PosAfter(const SubText, Text: String; StartAt: Integer): Integer;
  16.     var
  17.       I, L1, L2: Integer;
  18.     begin
  19.       Result := -1;
  20.       L1 := Length(SubText);
  21.       L2 := Length(Text);
  22.       if ((L1 > L2) or (StartAt < 1)) then exit;
  23.       for I := StartAt to L2-L1+1 do
  24.         if (CompareMem(@SubText[1], @Text[I], L1)) then
  25.         begin
  26.           Result := I;
  27.           exit;
  28.         end;
  29.     end;
  30.  
  31.     procedure TStringSplitter.SetCol(AValue: Integer);
  32.     begin
  33.       FCol := AValue;
  34.     end;
  35.  
  36.     procedure TStringSplitter.SetLine(AValue: String);
  37.     begin
  38.       FLine := AValue;
  39.       FCol := 1;
  40.     end;
  41.  
  42.     constructor TStringSplitter.Create;
  43.     begin
  44.       FLine := '';
  45.       FCol := 1;
  46.     end;
  47.  
  48.     function TStringSplitter.Fetch(const ATerminator: String): String;
  49.     var
  50.       I, J: Integer;
  51.     begin
  52.       Result := '';
  53.       I := FCol;
  54.       if (I > Length(FLine)) then exit;
  55.       if (ATerminator <> '') then
  56.       begin
  57.         J := PosAfter(ATerminator, FLine, I);
  58.         if (J > 0) then
  59.         begin
  60.           Result := Copy(FLine, I, J-I);
  61.           FCol := J + Length(ATerminator);
  62.           exit;
  63.         end;
  64.       end;
  65.       Result := Copy(FLine, FCol, MaxInt);
  66.       FCol := Length(FLine) + 1;
  67.     end;

When the server is started it initialize the socket to listen to the specified port. Then it repeatly check for incoming connection and start HTTP handler worker thread until the server is terminated. 

  1. procedure TDaemon1.WorkServerListener(Thread: TWorkerThread);
  2. var
  3.   sock: TTCPBlockSocket;
  4.   h: TSocket;
  5. begin
  6.   try
  7.     sock := TTCPBlockSocket.Create;
  8.     try
  9.       sock.Bind('0.0.0.0',IntToStr(Port));
  10.       if (sock.LastError <> 0) then
  11.         raise ENetworkError.Create('Cannot bind to port '+IntToStr(Port));
  12.       sock.Listen;
  13.       if (sock.LastError <> 0) then
  14.         raise ENetworkError.Create('Cannot listen to port '+IntToStr(Port));
  15.       while (not Thread.Terminated) do
  16.       begin
  17.         if (sock.CanRead(5000)) then
  18.         begin
  19.           h := sock.Accept;
  20.           {$HINTS OFF}
  21.           if (h <> INVALID_SOCKET) then
  22.             WorkerThreads.AddNew(@WorkRequestHandler,Pointer(h));
  23.           {$HINTS ON}
  24.         end;
  25.       end;
  26.     finally
  27.       sock.Free; // close and free socket object
  28.     end;
  29.   except
  30.     on E: Exception do
  31.       // DebugLog('HTTP Server terminated by error '+E.ClassName+': '+
  32.       //   E.Message);
  33.   end;
  34. end;

The HTTP handler worker thread read HTTP request from incoming connection and send back HTTP Respond.

  1. procedure TDaemon1.WorkRequestHandler(Thread: TWorkerThread);
  2. var
  3.   sock: TTCPBlockSocket;
  4.   S1: TStringSplitter;
  5.   s, cmd, url, http_ver, nm, vl, error_msg, respond_text, respond_data: AnsiString;
  6.   I, result_code: Integer;
  7.   close_socket, can_read: Boolean;
  8.   headers, gets, respond_headers: TStrings;
  9. begin
  10.   result_code := 500;
  11.   close_socket := True;
  12.   sock := TTCPBlockSocket.Create;
  13.   S1 := TStringSplitter.Create;
  14.   headers := TStringList.Create;
  15.   gets := TStringList.Create; 
  16.   respond_headers := TStringList.Create;
  17.   try
  18.     {$HINTS OFF}
  19.     sock.Socket := TSocket(Thread.Param);
  20.     {$HINTS ON}
  21.     repeat
  22.       can_read := sock.CanRead(5000);
  23.       if (sock.LastError <> 0) then exit;
  24.       if (can_read) then
  25.       begin
  26.         I := Pos('?',url);
  27.         if (I >= 1) then
  28.         begin
  29.          S1.Line := Copy(url, I+1, MaxInt);
  30.          repeat
  31.             nm := S1.Fetch('=');
  32.             if (nm <> '') then
  33.             begin
  34.               vl := S1.Fetch('&');
  35.               gets.Values[nm] := vl;  
  36.             end;
  37.           until (nm = '');
  38.           Delete(url,I,MaxInt); 
  39.         end;  
  40.         S1.Line := sock.RecvString(ReadTimeout);
  41.         if (sock.LastError <> 0) then exit;
  42.         cmd := S1.Fetch(' ');
  43.         url := S1.Fetch(' ');
  44.         http_ver := S1.Fetch;
  45.         if ((cmd = 'GET') or (cmd = 'POST')) then
  46.         begin
  47.           close_socket := (http_ver = 'HTTP/0.9') or (http_ver = 'HTTP/1.0');
  48.           S1.Line := sock.RecvString(ReadTimeout);
  49.           if (sock.LastError <> 0) then exit;
  50.           while (S1.Line <> '') do
  51.           begin
  52.             nm := S1.Fetch(': ');
  53.             vl := S1.Fetch;
  54.             headers.Values[nm] := vl;
  55.             S1.Line := sock.RecvString(ReadTimeout); 
  56.             if (sock.LastError <> 0) then exit; 
  57.           end;
  58.           //
  59.           // .. do some processing here
  60.           //
  61.           respond_data :=
  62.             '<head><title>Welcome</title></head><body><h1>Welcome to my Web Server</h1><p>You are in here: '+HttpEncode(url)+'</body>';
  63.           respond_headers.Values['Content-Type'] := 'html/text';
  64.           respond_headers.Values['Content-Length'] := IntToStr(Length(respond_data));
  65.           result_code := 200;
  66.         end
  67.         else begin
  68.           result_code := 501;
  69.           close_socket := True; 
  70.         end;
  71.        if((result_code < 200) or (result_code > 499)) then close_socket := True;
  72.         case result_code of
  73.         200: error_msg := 'OK';
  74.         400: error_msg := 'Bad Reqeust';
  75.         500: error_msg := 'Internal Server Error'; 
  76.         501: error_msg := 'Not Implemented';   
  77.         end;  
  78.         respond_text :=
  79.           http_ver + ' ' + IntToStr(result_code) + ' ' + error_msg + #13#10;
  80.         for I := 0 to respond_headers.Count do
  81.           respond_text := respond_text + respond_headers.Names[I] + ': ' +
  82.             respond_headers.ValueFromIndex[I] + #13#10;
  83.         respond_text := respond_text + #13#10 + respond_data;
  84.         sock.SendBuffer(@respond_text[1], Length(respond_text));
  85.         headers.Clear;
  86.         respond_headers.Clear;
  87.         respond_text := '';
  88.       end;
  89.     until (Thread.Terminated or close_socket);
  90.   finally
  91.     respond_headers.Free;
  92.     gets.Free;
  93.     headers.Free;
  94.     S1.Free;
  95.     sock.Free;
  96.   end;
  97. end;

You can send and receive data using easy to parse data like JSON, XML, BSON or binary data for data service, it is very flexible.

That's all for this article and I hope it's useful.

Kamis, 04 Desember 2025

Creating Thread from Procedure to simplify thread programming in Free Pascal and Delphi

 

Thread in Free Pascal and Delphi is a process that run simutanously with main program. This is very useful when we need to process data in background while user doing his own work. And is needed to maximize processing in multicore processor because a process including main process can only use 1 core and a quad core processor require a minimum of 4 threads to use all 4 cores.

Basically to create a thread we need to create the thread class and override the execute procedure with the thread code. To minimize work I create WorkerThreadList class, a very simple class to run a procedure of object as a thread.

First the class definition: 

  1. type 
  2.   { TSemaphore }
  3.   TSemaphore = class(TObject) 
  4.   private
  5.     FSem: Integer;
  6.   public
  7.     constructor Create;
  8.     destructor Destroy; override;
  9.     procedure Lock;
  10.     procedure Unlock;
  11.   end;
  12.  
  13.   { WorkerThreads Routines }
  14.   TWorkerThread = class;
  15.   TWorkerThreadList = class;
  16.  
  17.   TWorkEvent = procedure(WT: TWorkerThread) of object;
  18.  
  19.   { TWorkerThread }
  20.   TWorkerThread = class(TThread)
  21.   private
  22.     FOwner: TWorkerThreadList;
  23.     FOnWork: TWorkEvent;
  24.     FParam: Pointer;
  25.   protected
  26.     procedure Execute; override;
  27.   public
  28.     constructor Create(AOwner: TWorkerThreadList; AOnWork: TWorkEvent;
  29.       AParam: Pointer);
  30.     destructor Destroy; override;
  31.     property OnWork: TWorkEvent read FOnWork write FOnWork;
  32.     property Param: Pointer read FParam;
  33.     property Terminated;
  34.   end;
  35.  
  36.   { TWorkerThreadList }
  37.   TWorkerThreadList = class(TObject)
  38.   private
  39.     FWorkers: TObjectList;
  40.     FSem: TSemaphore;
  41.     function GetItems(Index: Integer): TWorkerThread;
  42.   public
  43.     constructor Create;
  44.     destructor Destroy; override;
  45.     function AddNew(AOnWork: TWorkEvent; AParam: Pointer=nil): TWorkerThread;
  46.     procedure TerminateWait(Thread: TWorkerThread);
  47.     procedure UnregisterWorkerThread(AThread: TWorkerThread);
  48.     procedure TerminateWorkerThreads(Wait: Boolean);
  49.     property Items[Index: Integer]: TWorkerThread read GetItems; default;
  50.   end;

Then I create the class definition like this:

  1. { TWorkerThreadList }
  2.  
  3. function TWorkerThreadList.GetItems(Index: Integer): TWorkerThread;
  4. begin
  5.   Result := TWorkerThread(FWorkers[Index]);
  6. end;
  7.  
  8. constructor TWorkerThreadList.Create;
  9. begin
  10.   inherited;
  11.   FWorkers := TObjectList.Create(False);
  12.   FSem := TSemaphore.Create;
  13. end;
  14.  
  15. destructor TWorkerThreadList.Destroy;
  16. begin
  17.   TerminateWorkerThreads(True);
  18.   FWorkers.Free;
  19.   FSem.Free;
  20.   inherited Destroy;
  21. end;
  22.  
  23. function TWorkerThreadList.AddNew(AOnWork: TWorkEvent;
  24.   AParam: Pointer): TWorkerThread;
  25. var
  26.   N: TWorkerThread;
  27. begin
  28.   N := TWorkerThread.Create(Self, AOnWork, AParam);
  29.   try
  30.     FSem.Lock;
  31.     try
  32.       FWorkers.Add(N);
  33.     finally
  34.       FSem.Unlock;
  35.     end;
  36.     Result := N;
  37.     N.Start;
  38.   except
  39.     N.Free;
  40.     raise;
  41.   end;
  42. end;
  43.  
  44. procedure TWorkerThreadList.TerminateWait(Thread: TWorkerThread);
  45. var
  46.   I: Integer; 
  47. begin
  48.   if (Thread = nil) then exit;
  49.   FSem.Lock;
  50.   try
  51.     if (FWorkers.IndexOf(Thread) < 0) then
  52.       exit
  53.     else begin
  54.       if (not Thread.Terminated) then Thread.Terminate;
  55.       if (Thread.Suspended) then Thread.Start;
  56.     end;
  57.   finally
  58.     FSem.Unlock;
  59.   end;
  60.   repeat
  61.     FSem.Lock;
  62.     try
  63.       I := FWorkers.IndexOf(Thread); 
  64.       if (I < 0) then exit;
  65.       if (Thread.Finished or (Thread.ThreadId = 0)) then
  66.       begin
  67.         FWorkers.Remove(Thread); 
  68.         Thread.FOwner := nil;
  69.         Thread.Free; 
  70.         exit; 
  71.       end; 
  72.     finally
  73.       FSem.Unlock;
  74.     end;
  75.     Sleep(1);
  76.   until (False);
  77. end;
  78.  
  79. procedure TWorkerThreadList.UnregisterWorkerThread(AThread: TWorkerThread);
  80. begin
  81.   if ((AThread = nil) or (AThread.FOwner <> Self)) then
  82.     exit;
  83.   FSem.Lock;
  84.   try
  85.     FWorkers.Remove(AThread);
  86.     AThread.FOwner := nil;
  87.   finally
  88.     FSem.Unlock;
  89.   end;
  90. end;
  91.  
  92. procedure TWorkerThreadList.TerminateWorkerThreads(Wait: Boolean);
  93. var
  94.   I, J: Integer;
  95. begin
  96.   repeat
  97.     FSem.Lock;
  98.     try
  99.       J := 0;
  100.       for I := FWorkers.Count-1 downto 0 do
  101.       begin
  102.         with TWorkerThread(FWorkers[I]) do
  103.         begin
  104.           if (not Finished and (ThreadId <> 0)) then
  105.           begin 
  106.             if (not Terminated) then Terminate;
  107.             if (Suspended) then Start;
  108.             Inc(J); 
  109.           end; 
  110.         end;
  111.       end;
  112.     finally
  113.       FSem.Unlock;
  114.     end; 
  115.     if (J > 0) then Sleep(70);
  116.   until ((J = 0) or not Wait);
  117.  
  118.   FSem.Lock; 
  119.   try
  120.     for I := FWorkers.Count-1 downto 0 do 
  121.     begin
  122.       try 
  123.         TWorkerThread(FWorkers[I]).FOwner := nil;
  124.         TWorkerThread(FWorkers[I]).Free; 
  125.       except
  126.         // 
  127.       end; 
  128.     end; 
  129.     FWorkers.Clear; 
  130.   finally
  131.     FSem.Unlock; 
  132.   end; 
  133. end;
  134.  
  135. { TWorkerThread }
  136.  
  137. procedure TWorkerThread.Execute;
  138. begin
  139.   try
  140.     if (Assigned(FOnWork)) then FOnWork(Self);
  141.   except
  142.   end;
  143.   if (FOwner <> nil) then FOwner.UnregisterWorkerThread(Self);
  144. end;
  145.  
  146. constructor TWorkerThread.Create(AOwner: TWorkerThreadList; AOnWork: TWorkEvent;
  147.   AParam: Pointer);
  148. begin
  149.   inherited Create(True);
  150.   FreeOnTerminate := True;
  151.   Priority := tpIdle;
  152.   FOwner := AOwner;
  153.   FOnWork := AOnWork;
  154.   FParam := AParam;
  155. end;
  156.  
  157. destructor TWorkerThread.Destroy;
  158. begin
  159.   if (FOwner <> nil) then FOwner.UnregisterWorkerThread(Self);
  160.   inherited Destroy;
  161. end;
  162.  
  163. { TSemaphore }
  164. constructor TSemaphore.Create;
  165. begin
  166.   FSem := 0;
  167. end;
  168. destructor TSemaphore.Destroy;
  169. begin
  170.   Unlock;
  171.   inherited Destroy;
  172. end;
  173. procedure TSemaphore.Lock;
  174. begin
  175.   while (InterLockedExchange(FSem, 1) = 1) do Sleep(1);
  176. end;
  177.  
  178. procedure TSemaphore.Unlock;
  179. begin
  180.   InterLockedExchange(FSem, 0);
  181. end; 

Using this class I can run a procedure of object with this parameter using TWorkerThreadList object:

  TWorkEvent = procedure(WT: TWorkerThread) of object;

How to use the TWorkerThreadList class:

  • Define WorkerThreads variable as global variable or in the data module:

        WorkerThreads: TWorkerThreadList;

  •  Add object create and free for the WorkerThreads object in the data module on create and on destroy:
    1. procedure TDaemon1.DataModuleCreate(Sender: TObject);
    2. begin
    3.   WorkerThreads := TWorkerThreadList.Create;
    4. end;
    5.  
    6. procedure TDaemon1.DataModuleDestroy(Sender: TObject);
    7. begin
    8.   WorkerThreads.Free;
    9. end;
  • Create the thread procedure:
    1. procedure TDaemon1.WorkServerListener(Thread: TWorkerThread);
    2. begin
    3.   try
    4.     { do something } 
    5.   except
    6.     { do handle exceptions }
    7.   end;
    8. end;
  •  Create thread for the procedure:
    1. procedure TDaemon1.DataModuleStart(Sender: TCustomDaemon; var OK: Boolean
    2.   );
    3. begin
    4.   WorkerThreads.AddNew(@WorkServerListener, nil);
    5.   OK := True; 
    6. end;

WorkerThreadList object can be created and destroyed as needed. A WorkerThreadList object can run alot of procedures.

Working with thread can be tricky, You have to be carefull with race condition, some objects that is not thread safe you need to put semaphore so that only one thread can access it at a time. Memory leak is not the only bugs.

Thats all for the WorkerThreadList object, and I hope this article can be useful.

Building New PC To Study LLM

Since early 2025, I've been using AI LLMs like ChatGPT or DeepSeek to learn programming or help with research. Using AI LLMs is easier b...