Asynchronous Chilkat Operations in Lazarus / Free Pascal

running Chilkat methods on a background thread with TThread

Applies to Chilkat for Lazarus / Free Pascal on Windows, Linux, and macOS.

The release notes for Chilkat for Lazarus state that the API is synchronous only: there are no *Async method variants, and no Task or TaskChain classes. The most common question about that note is whether it describes a transitional state. It does not.

The short answer

Chilkat for Lazarus will not add *Async, Task, or TaskChain. This is a deliberate design decision, not a gap to be filled in a later release.

Chilkat's position going forward is that asynchronous behavior belongs in the programming language, with Chilkat's ordinary synchronous methods called from whatever thread the language gives you. In Object Pascal that language facility is TThread, and it is a better tool than the Chilkat task system was: you control the thread, you control its lifetime, and progress and completion arrive on the UI thread through the mechanism the LCL already provides.

Nothing is lost. Everything Task did — background execution, progress reporting, cancellation, chaining, and running many operations in parallel — is a few lines of standard Pascal. This page shows each one.

Why the async API is not being carried forward

Chilkat's *Async methods worked by running the operation on a background thread inside Chilkat. That design has consequences that get worse, not better, over time:

  • The TaskCompleted callback fires on Chilkat's internal thread, not on your application's main thread. In a GUI framework such as the LCL, touching a control from that callback is undefined behavior. Every correct program therefore had to marshal the result back to the main thread anyway — so the thread-hopping was never actually avoided, only hidden.
  • The task system was a second, parallel scheduling model that had to be learned in addition to the one your language already has. Two thread pools, two cancellation mechanisms, two ways to sequence work.
  • It could not compose with anything else in your program. A Chilkat TaskChain could chain Chilkat calls, but it could not interleave a database write, a file rename, or a call into another library.

Doing the concurrency in Pascal removes all three problems at once. There is one thread model in your application, one place where work is cancelled, and Chilkat calls sit alongside every other call you make.

What each Chilkat task feature becomes

Chilkat async/task featureLazarus / FPC equivalent
obj.SomeMethodAsync(...) returning a Task Call obj.SomeMethod(...) inside TThread.Execute
task.Run / task.RunOnThreadPool Create the TThread (or hand work to a pool of them)
task.Wait(maxWaitMs) thread.WaitFor
task.Cancel thread.Terminate, observed in OnAbortCheck
task.PercentDone / TaskCompleted callback Chilkat's OnPercentDone event plus Queue / Synchronize
task.GetResultString / GetResultBool A field on your thread class, read after WaitFor or in the completion handler
TaskChain (step 1, then step 2, then step 3) Consecutive statements in one Execute method
ThreadPool / Global.MaxThreads However many TThread instances you choose to create

Five rules for calling Chilkat from a thread

  1. Unlock once, on the main thread, before any worker starts. TGlobal.UnlockBundle unlocks every Chilkat class in the process for the life of the application. Do not call it from each thread.
  2. Give each thread its own Chilkat objects. Create them in Execute and free them in Execute. A single THttp or TSFtp instance must not be used by two threads at the same time. Separate instances in separate threads are fully supported — that is exactly what Chilkat's own thread pool used to do.
  3. Remember that Chilkat events fire on the calling thread. OnPercentDone, OnAbortCheck, and OnProgressInfo are invoked by the Chilkat method itself, so inside a worker thread they run on that worker. Never touch an LCL control from them directly.
  4. Marshal to the UI thread with Queue or Synchronize. Queue posts the call and returns immediately — right for frequent progress updates, since it never stalls the transfer. Synchronize blocks the worker until the main thread has run the method — right for a final result you want delivered before the thread ends.
  5. Cancel through OnAbortCheck. Set HeartbeatMs so the event fires periodically, and return Terminated from the handler. Returning True makes the running Chilkat method return early with a failure status.
A note on compiler mode. Every example below begins with {$mode delphi}, which is also what the Chilkat units use. In that mode a method is assigned to an event with no operator: http.OnPercentDone := HttpPercentDone;. If your unit uses {$mode objfpc}{$H+} instead — the default for a new Lazarus form unit — write http.OnPercentDone := @HttpPercentDone; with the @. Everything else is identical.

Example 1: one background operation, with progress and cancel

This is the direct replacement for calling a *Async method and waiting for TaskCompleted. A worker thread downloads a URL with THttp.Download, reports progress to a progress bar, and stops promptly when the user clicks Cancel.

The worker thread unit

unit DownloadThread;

{$mode delphi}{$H+}

interface

uses
  Classes, SysUtils, Chilkat.Base, Chilkat.Http;

type
  TDownloadProgressEvent = procedure(Sender: TObject; PctDone: Integer) of object;
  TDownloadDoneEvent = procedure(Sender: TObject; Success: Boolean;
    const ErrorText: string) of object;

  TDownloadThread = class(TThread)
  private
    FUrl: string;
    FLocalPath: string;
    FSuccess: Boolean;
    FErrorText: string;
    FPctDone: Integer;
    FOnProgress: TDownloadProgressEvent;
    FOnDone: TDownloadDoneEvent;
    // Chilkat event handlers.  These run on THIS thread, not the main thread.
    function HttpPercentDone(Sender: TChilkatBase; PctDone: Integer): Boolean;
    function HttpAbortCheck(Sender: TChilkatBase): Boolean;
    // These two run on the main thread, via Queue / Synchronize.
    procedure ReportProgress;
    procedure ReportDone;
  protected
    procedure Execute; override;
  public
    constructor Create(const AUrl, ALocalPath: string;
      AOnProgress: TDownloadProgressEvent; AOnDone: TDownloadDoneEvent);
  end;

implementation

constructor TDownloadThread.Create(const AUrl, ALocalPath: string;
  AOnProgress: TDownloadProgressEvent; AOnDone: TDownloadDoneEvent);
begin
  // Assign every field BEFORE calling the inherited constructor with
  // CreateSuspended = False, because the thread may begin running
  // the moment that constructor returns.
  FUrl := AUrl;
  FLocalPath := ALocalPath;
  FOnProgress := AOnProgress;
  FOnDone := AOnDone;
  FSuccess := False;
  FreeOnTerminate := True;
  inherited Create(False);
end;

procedure TDownloadThread.Execute;
var
  http: THttp;
begin
  // Each thread creates, uses, and frees its own Chilkat objects.
  http := THttp.Create;
  try
    if not http.IsValid then
    begin
      FErrorText := 'The Chilkat shared library could not be loaded.';
      Synchronize(ReportDone);
      Exit;
    end;

    http.OnPercentDone := HttpPercentDone;
    http.OnAbortCheck := HttpAbortCheck;
    http.HeartbeatMs := 200;   // OnAbortCheck fires 5 times per second

    // A perfectly ordinary synchronous Chilkat call -- it just happens
    // to be running on a background thread.
    FSuccess := http.Download(FUrl, FLocalPath);
    if not FSuccess then
      FErrorText := http.LastErrorText;
  finally
    http.Free;
  end;

  // Synchronize (not Queue) for the final notification: it is guaranteed
  // to have run before this thread object is destroyed.
  Synchronize(ReportDone);
end;

function TDownloadThread.HttpPercentDone(Sender: TChilkatBase;
  PctDone: Integer): Boolean;
begin
  FPctDone := PctDone;
  Queue(ReportProgress);   // does not block the download
  Result := Terminated;    // True aborts the transfer
end;

function TDownloadThread.HttpAbortCheck(Sender: TChilkatBase): Boolean;
begin
  // Called every HeartbeatMs milliseconds, even while blocked on the
  // network, so Cancel takes effect within a fraction of a second.
  Result := Terminated;
end;

procedure TDownloadThread.ReportProgress;
begin
  if Assigned(FOnProgress) then FOnProgress(Self, FPctDone);
end;

procedure TDownloadThread.ReportDone;
begin
  if Assigned(FOnDone) then FOnDone(Self, FSuccess, FErrorText);
end;

end.

Using it from a form

unit MainForm;

{$mode delphi}{$H+}

interface

uses
  Classes, SysUtils, Forms, StdCtrls, ComCtrls, DownloadThread;

type
  TForm1 = class(TForm)
    btnStart: TButton;
    btnCancel: TButton;
    ProgressBar1: TProgressBar;
    lblStatus: TLabel;
    procedure btnStartClick(Sender: TObject);
    procedure btnCancelClick(Sender: TObject);
  private
    FDownload: TDownloadThread;
    procedure DownloadProgress(Sender: TObject; PctDone: Integer);
    procedure DownloadDone(Sender: TObject; Success: Boolean;
      const ErrorText: string);
  end;

implementation

{$R *.lfm}

procedure TForm1.btnStartClick(Sender: TObject);
begin
  if FDownload <> nil then Exit;          // one at a time

  ProgressBar1.Position := 0;
  lblStatus.Caption := 'Downloading...';
  btnStart.Enabled := False;
  btnCancel.Enabled := True;

  // The form does not block.  It stays responsive while this runs.
  FDownload := TDownloadThread.Create(
    'https://www.example.com/bigfile.zip', 'bigfile.zip',
    DownloadProgress, DownloadDone);
end;

procedure TForm1.btnCancelClick(Sender: TObject);
begin
  if FDownload <> nil then
    FDownload.Terminate;     // OnAbortCheck picks this up
end;

procedure TForm1.DownloadProgress(Sender: TObject; PctDone: Integer);
begin
  // Runs on the main thread -- safe to touch controls.
  ProgressBar1.Position := PctDone;
end;

procedure TForm1.DownloadDone(Sender: TObject; Success: Boolean;
  const ErrorText: string);
begin
  // Also on the main thread.
  FDownload := nil;          // FreeOnTerminate frees the object itself
  btnStart.Enabled := True;
  btnCancel.Enabled := False;
  if Success then
    lblStatus.Caption := 'Done.'
  else
    lblStatus.Caption := 'Failed: ' + ErrorText;
end;

end.
Where does UnlockBundle go? In the form's OnCreate, or in the .lpr before Application.Run — anywhere on the main thread at startup. Unlocking is process-wide and permanent, so the worker threads inherit it automatically.
glob := TGlobal.Create;
try
  if not glob.UnlockBundle('Anything for 30-day trial') then
    ShowMessage(glob.LastErrorText);
finally
  glob.Free;
end;

Example 2: replacing TaskChain

A TaskChain ran a sequence of operations one after another on a background thread, stopping if a step failed. In Pascal that is simply a sequence of statements inside Execute — with the advantage that the steps can be anything at all, not only Chilkat calls.

procedure TUploadJobThread.Execute;
var
  sftp: TSFtp;
  i: Integer;
begin
  sftp := TSFtp.Create;
  try
    sftp.ConnectTimeoutMs := 10000;
    sftp.IdleTimeoutMs := 20000;

    // Step 1
    FStatus := 'Connecting...';
    Queue(ReportStatus);
    if not sftp.Connect('sftp.example.com', 22) then
      begin FErrorText := sftp.LastErrorText; Exit; end;

    // Step 2
    if not sftp.AuthenticatePw('mylogin', 'mypassword') then
      begin FErrorText := sftp.LastErrorText; Exit; end;

    // Step 3
    if not sftp.InitializeSftp then
      begin FErrorText := sftp.LastErrorText; Exit; end;

    // Step 4 -- a loop, which no TaskChain could express
    for i := 0 to FFiles.Count - 1 do
    begin
      if Terminated then Exit;

      FStatus := Format('Uploading %d of %d...', [i + 1, FFiles.Count]);
      Queue(ReportStatus);

      if not sftp.UploadFileByName('/upload/' + ExtractFileName(FFiles[i]),
                                   FFiles[i]) then
      begin
        FErrorText := sftp.LastErrorText;
        Exit;
      end;

      // ...and here you can do anything else at all: write a database
      // row, move the local file to an archive folder, log the result.
      RenameFile(FFiles[i], FArchiveDir + ExtractFileName(FFiles[i]));
    end;

    sftp.Disconnect;
    FSuccess := True;
  finally
    sftp.Free;
    Synchronize(ReportDone);   // runs whether we succeeded or bailed out
  end;
end;

Reading top to bottom tells you exactly what happens and in what order. There is no chain object to build, no callback to register, and the failure path is an ordinary Exit.

Example 3: many operations in parallel

This replaces RunOnThreadPool and Global.MaxThreads. A fixed number of worker threads pull URLs from a shared, lock-protected queue; the main thread waits for all of them and totals the results. This is a complete, compilable console program.

program ParallelDownloads;

{$mode delphi}{$H+}

uses
  {$IFDEF UNIX}cthreads,{$ENDIF}       // REQUIRED on Linux and macOS
  Classes, SysUtils, SyncObjs,
  Chilkat.Global, Chilkat.Http;

type
  { A trivial thread-safe work queue. }
  TWorkQueue = class
  private
    FLock: TCriticalSection;
    FItems: TStringList;
    FNext: Integer;
  public
    constructor Create(AItems: TStrings);
    destructor Destroy; override;
    function TryGetNext(out AUrl: string): Boolean;
  end;

  TDownloadWorker = class(TThread)
  private
    FQueue: TWorkQueue;
    FOkCount: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(AQueue: TWorkQueue);
    property OkCount: Integer read FOkCount;
  end;

constructor TWorkQueue.Create(AItems: TStrings);
begin
  inherited Create;
  FLock := TCriticalSection.Create;
  FItems := TStringList.Create;
  FItems.Assign(AItems);
  FNext := 0;
end;

destructor TWorkQueue.Destroy;
begin
  FItems.Free;
  FLock.Free;
  inherited Destroy;
end;

function TWorkQueue.TryGetNext(out AUrl: string): Boolean;
begin
  FLock.Acquire;
  try
    Result := FNext < FItems.Count;
    if Result then
    begin
      AUrl := FItems[FNext];
      Inc(FNext);
    end;
  finally
    FLock.Release;
  end;
end;

constructor TDownloadWorker.Create(AQueue: TWorkQueue);
begin
  FQueue := AQueue;
  FOkCount := 0;
  FreeOnTerminate := False;   // the main thread calls WaitFor, then frees
  inherited Create(False);
end;

procedure TDownloadWorker.Execute;
var
  http: THttp;
  url: string;
begin
  // One THttp per worker.  Never share a Chilkat object across threads.
  http := THttp.Create;
  try
    while (not Terminated) and FQueue.TryGetNext(url) do
    begin
      if http.Download(url, ExtractFileName(url)) then
        Inc(FOkCount)
      else
        WriteLn('FAILED ', url, ' : ', http.LastErrorText);
    end;
  finally
    http.Free;
  end;
end;

const
  NUM_WORKERS = 4;

var
  glob: TGlobal;
  urls: TStringList;
  queue: TWorkQueue;
  workers: array[0..NUM_WORKERS - 1] of TDownloadWorker;
  i, urlCount, okTotal: Integer;

begin
  // 1) Unlock once, on the main thread, before starting any worker.
  glob := TGlobal.Create;
  try
    if not glob.UnlockBundle('Anything for 30-day trial') then
    begin
      WriteLn(glob.LastErrorText);
      Halt(1);
    end;
  finally
    glob.Free;
  end;

  urls := TStringList.Create;
  try
    urls.Add('https://www.example.com/file1.zip');
    urls.Add('https://www.example.com/file2.zip');
    urls.Add('https://www.example.com/file3.zip');
    urls.Add('https://www.example.com/file4.zip');
    urls.Add('https://www.example.com/file5.zip');
    urlCount := urls.Count;

    queue := TWorkQueue.Create(urls);
    try
      // 2) Start the workers.  Four downloads run concurrently.
      for i := 0 to NUM_WORKERS - 1 do
        workers[i] := TDownloadWorker.Create(queue);

      // 3) Wait for all of them -- the equivalent of task.Wait().
      okTotal := 0;
      for i := 0 to NUM_WORKERS - 1 do
      begin
        workers[i].WaitFor;
        Inc(okTotal, workers[i].OkCount);
        workers[i].Free;
      end;
    finally
      queue.Free;
    end;
  finally
    urls.Free;
  end;

  WriteLn(Format('%d of %d downloads succeeded.', [okTotal, urlCount]));
end.
On Linux and macOS, a multithreaded FPC program must use cthreads. It has to be the first unit in the program's uses clause, as shown above. Without it the program compiles but threading does not work correctly at run time. Lazarus GUI projects created from the LCL templates already include this in the .lpr; console projects usually do not.
Console programs and Synchronize. Synchronize and Queue hand work to the main thread's message loop. An LCL application has one. A console program does not, so a queued method never runs and Synchronize blocks forever. In a console program, either avoid them entirely (as Example 3 does — the workers just write to the console and return counts) or call CheckSynchronize periodically from the main thread while it waits.

Next steps