Pacific Blue Software Logo

How to Handle Nil Pointers in TMS WebCore?

How to handle Nil Pointers in TMS WebCore?

Introduction

When working with TMS WebCore, you'll encounter scenarios where you need to pass procedures as parameters to other procedures or methods. This is common in callback patterns, event handlers, and functional programming approaches. However, handling nil (null) procedure pointers in WebCore works differently than in standard Delphi or when using modern anonymous methods.

The key difference is that TMS WebCore uses procedure pointers (traditional Pascal style) rather than anonymous method references when you use the @ operator. This distinction is crucial for understanding why nil assignment behaves differently and how to properly handle optional callback procedures.

Understanding the Problem

In standard Delphi with anonymous methods, you can easily pass nil:

procedure TestProc(AProc: TProc);
begin
  if Assigned(AProc) then
    AProc;
end;

// This works with anonymous methods:
TestProc(nil);  // Works in VCL/FMX

However, in TMS WebCore when using procedure pointers with the @ operator, direct nil assignment may cause compiler errors or unexpected behavior:

// This may cause issues in WebCore:
TestProc(nil);  // Compiler error or runtime issue

This happens because WebCore's transpiler to JavaScript handles procedure pointers differently than anonymous method references. The @ProcedureName syntax creates a JavaScript function reference, not a Delphi object reference, so the nil semantics don't translate directly.

Solution 1: Use a Variable to Hold the Procedure Pointer

The most straightforward approach is to use a typed variable to hold the procedure pointer:

type
  TProcPointer = procedure;

var
  MyProcPtr: TProcPointer;

procedure TestProc(AProc: TProcPointer);
begin
  if Assigned(AProc) then
    AProc
  else
    WriteLn('Procedure pointer is nil');
end;

// Usage:
MyProcPtr := nil;
TestProc(MyProcPtr); // This works - variable can be nil

// Or direct assignment:
TestProc(nil); // This also works with the variable approach

Why This Works

By declaring a typed variable, you give the compiler explicit type information that allows it to properly handle the nil value. The variable serves as a strongly-typed container that can hold either a valid procedure reference or nil.

When to Use This Approach

Solution 2: Explicitly Cast nil

You can explicitly cast nil to the procedure pointer type:

type
  TProcPointer = procedure;

procedure TestProc(AProc: TProcPointer);
begin
  if Assigned(AProc) then
    AProc
  else
    WriteLn('No procedure provided');
end;

// Usage:
TestProc(TProcPointer(nil)); // Explicit cast makes intent clear

Advantages of Explicit Casting

When to Use This Approach

Solution 3: Use Optional Parameters with Default nil

The most elegant solution is often to make the procedure parameter optional with a default value of nil:

type
  TProcPointer = procedure;

procedure TestProc(AProc: TProcPointer = nil);
begin
  if Assigned(AProc) then
    AProc
  else
    WriteLn('Using default behavior');
end;

// Usage:
TestProc();                      // Calls with nil default - clean!
TestProc(@SomeActualProcedure);  // Calls with actual procedure

Benefits of Default Parameters

Best Practices with Default Parameters

When using default nil parameters, consider these patterns:

// Pattern 1: Provide default behavior when nil
procedure ProcessData(OnComplete: TProcPointer = nil);
begin
  // Do processing...
  
  if Assigned(OnComplete) then
    OnComplete
  else
    ShowMessage('Processing complete'); // Default behavior
end;

// Pattern 2: Make callback truly optional
procedure LoadData(const AUrl: string; OnSuccess: TProcPointer = nil);
begin
  // Fetch data...
  
  // Only call callback if provided
  if Assigned(OnSuccess) then
    OnSuccess;
end;

When to Use This Approach

Solution 4: Use Conditional Assignment

For complex scenarios where the procedure to call depends on runtime conditions:

type
  TProcPointer = procedure;

var
  ProcToPass: TProcPointer;

procedure HandleSuccess;
begin
  WriteLn('Success handler called');
end;

procedure HandleError;
begin
  WriteLn('Error handler called');
end;

procedure TestProc(AProc: TProcPointer);
begin
  if Assigned(AProc) then
    AProc;
end;

// Conditionally assign based on application state
if SomeCondition then
  ProcToPass := @HandleSuccess
else if AnotherCondition then
  ProcToPass := @HandleError
else
  ProcToPass := nil;  // No handler for this case

TestProc(ProcToPass);

Advanced Pattern: Procedure Pointer Tables

You can use arrays or records to organize related procedure pointers:

type
  TProcPointer = procedure;
  
  TEventHandlers = record
    OnSuccess: TProcPointer;
    OnError: TProcPointer;
    OnComplete: TProcPointer;
  end;

var
  Handlers: TEventHandlers;

procedure InitializeHandlers;
begin
  Handlers.OnSuccess := @HandleSuccess;
  Handlers.OnError := @HandleError;
  Handlers.OnComplete := nil;  // Optional handler
end;

procedure ExecuteWithHandlers(const AHandlers: TEventHandlers);
begin
  // Execute some operation...
  
  if Success then
  begin
    if Assigned(AHandlers.OnSuccess) then
      AHandlers.OnSuccess;
  end
  else
  begin
    if Assigned(AHandlers.OnError) then
      AHandlers.OnError;
  end;
  
  // Always call completion if provided
  if Assigned(AHandlers.OnComplete) then
    AHandlers.OnComplete;
end;

When to Use This Approach

Understanding Procedure Pointers vs Anonymous Methods

Procedure Pointers (Traditional Pascal)

When you use @ProcedureName in WebCore:

Anonymous Methods (Modern Delphi)

Standard Delphi anonymous methods:

Why the Difference Matters in WebCore

TMS WebCore transpiles Delphi code to JavaScript. JavaScript functions work more like traditional procedure pointers than Delphi's modern anonymous methods. This is why WebCore uses the @ operator syntax - it maps more naturally to JavaScript's function semantics.

Common Pitfalls and How to Avoid Them

Pitfall 1: Forgetting to Check Assigned

// WRONG - Will crash if AProc is nil
procedure TestProc(AProc: TProcPointer);
begin
  AProc;  // Crash if nil!
end;

// CORRECT - Always check
procedure TestProc(AProc: TProcPointer);
begin
  if Assigned(AProc) then
    AProc;
end;

Pitfall 2: Mixing Procedure Pointer Styles

// WRONG - Inconsistent types
type
  TCallback1 = procedure;
  TCallback2 = procedure of object;  // Different type!

// CORRECT - Use consistent types
type
  TCallback = procedure;

Pitfall 3: Scope Issues with Nested Procedures

// WRONG - Nested procedure reference may not be valid
procedure Outer;
  procedure Nested;
  begin
    WriteLn('Nested');
  end;
var
  Proc: TProcPointer;
begin
  Proc := @Nested;  // May cause issues
end;

// CORRECT - Use method pointers for class methods
type
  TMyClass = class
    procedure MyMethod;
  end;
  
var
  Obj: TMyClass;
  Proc: TProcPointer;
  
Proc := @Obj.MyMethod;  // This works

Real-World Example: Async Data Loading

Here's a practical example showing how to use optional procedure pointers for asynchronous operations:

type
  TDataLoadCallback = procedure;

procedure LoadCustomerData(const ACustomerId: Integer;
                          OnSuccess: TDataLoadCallback = nil;
                          OnError: TDataLoadCallback = nil);
begin
  // Simulate async XData call
  XDataClient.Get('/customers/' + IntToStr(ACustomerId),
    procedure(Response: TJSObject)
    begin
      // Process response...
      
      if Assigned(OnSuccess) then
        OnSuccess;
    end,
    procedure(Error: TJSObject)
    begin
      // Handle error...
      
      if Assigned(OnError) then
        OnError;
    end
  );
end;

// Usage examples:

// 1. With both callbacks
procedure HandleSuccess;
begin
  ShowMessage('Customer loaded successfully');
end;

procedure HandleError;
begin
  ShowMessage('Failed to load customer');
end;

LoadCustomerData(123, @HandleSuccess, @HandleError);

// 2. With only success callback
LoadCustomerData(123, @HandleSuccess);

// 3. With no callbacks (fire and forget)
LoadCustomerData(123);

Performance Considerations

Best Practices Summary

  1. Always use Assigned() before calling procedure pointers
  2. Use default nil parameters for optional callbacks
  3. Type your procedure pointers consistently throughout your codebase
  4. Document whether nil is allowed in procedure comments
  5. Use explicit casts when passing nil for clarity
  6. Consider using records to group related procedure pointers
  7. Keep it simple - Don't over-engineer callback systems

Related Topics


Working with Procedure Pointers and Nil Values in TMS WebCore


Back to Articles for Developers
Back to Articles on Delphi
Convert Left Click to Right Click in TMS WebCore
Implement Virtual Tables in TMS WebCore and XData

If you found this useful, then please consider making a donation.

paypal
QR Code for donation Please donate if helpful