Example of a Tcl extension in Free Pascal

dbohdan 2016-10-02: This example shows how to write a Tcl extension in Free Pascal. It has been tested with FPC 2.6.2-8 2014/01/22 and 3.0.0 2015/11/20 for x86_64 on Ubuntu 14.04.

Note that the example does not use the Tcl80 unit. That unit is outdated and breaks if you have a recent version of Tcl or target x86_64. It also lacks bindings for the *Obj* functions of the Tcl C API. (E.g., it has the legacy Tcl_CreateCommand but not Tcl_CreateObjCommand.) Instead, the extension contains its own bindings just for the functions it uses.

To build and test on Linux, use the POSIX shell command

fpc tclfpexample.pas \
&& echo 'load libtclfpexample.so; puts [hello]; puts [square 5]' | tclsh

tclfpexample.pas

library TclFPExample;

uses ctypes;

const
    STUBS = 'tclstub8.6';
    TCL_VERSION = '8.6';

    TCL_OK = 0;
    TCL_ERROR = 1;

type
    PPTcl_Obj = ^PTcl_Obj;
    PTcl_Interp = Pointer;
    PTcl_Obj = Pointer;
    Tcl_ClientData = Pointer;
    Tcl_CmdDeleteProc = Pointer;
    Tcl_ObjCmdProc = function(clientData: Tcl_ClientData;
                              interp: PTcl_Interp;
                              objc: cint;
                              objv: PPTcl_Obj): cint; cdecl;
    Tcl_Command = Pointer;

function Tcl_CreateObjCommand(interp: PTcl_Interp;
                              cmdName: PChar;
                              proc: Tcl_ObjCmdProc;
                              clientData: Tcl_ClientData;
                              deleteProc: Tcl_CmdDeleteProc): Tcl_Command;
         cdecl; external STUBS;
function Tcl_InitStubs(interp: PTcl_Interp;
                       version: PChar;
                       exact: cint): PChar;
         cdecl; external STUBS;
function Tcl_GetIntFromObj(interp: PTcl_Interp;
                           objPtr: PTcl_Obj;
                           intPtr: pcint): cint;
         cdecl; external STUBS;
function Tcl_NewIntObj(intValue: cint): PTcl_Obj;
         cdecl; external STUBS;
function Tcl_NewStringObj(bytes: PChar; length: cint): PTcl_Obj;
         cdecl; external STUBS;
procedure Tcl_SetObjResult(interp: PTcl_Interp;
                           resultObjPtr: PTcl_Obj);
          cdecl; external STUBS;
procedure Tcl_WrongNumArgs(interp: PTcl_Interp;
                          objc: cint;
                          objv: PPTcl_Obj;
                          message: PChar);
          cdecl; external STUBS;


function Hello_Cmd(clientData: Tcl_ClientData;
                   interp: PTcl_Interp;
                   objc: cint;
                   objv: PPTcl_Obj): cint; cdecl;
begin
    if objc <> 1 then
    begin
        Tcl_WrongNumArgs(interp, 1, objv, nil);
        Hello_Cmd := TCL_ERROR;
        exit;
    end;
    Tcl_SetObjResult(interp, Tcl_NewStringObj('Hello, World!', -1));
    Hello_Cmd := TCL_OK;
end;

function Square_Cmd(clientData: Tcl_ClientData;
                    interp: PTcl_Interp;
                    objc: cint;
                    objv: PPTcl_Obj): cint; cdecl;
var i: cint;
begin
    if objc <> 2 then
    begin
        Tcl_WrongNumArgs(interp, 1, objv, 'value');
        Square_Cmd := TCL_ERROR;
        exit;
    end;
    if Tcl_GetIntFromObj(interp, objv[1], @i) <> TCL_OK then
    begin
        Square_Cmd := TCL_ERROR;
        exit;
    end;
    Tcl_SetObjResult(interp, Tcl_NewIntObj(i*i));
    Square_Cmd := TCL_OK;
end;

function Tclfpexample_Init(interp: PTcl_Interp): cint; cdecl;
begin
    if Tcl_InitStubs(interp, TCL_VERSION, 0) = nil then
    begin
        Tclfpexample_Init := TCL_ERROR;
        exit;
    end;

    Tcl_CreateObjCommand(interp, 'hello', Tcl_ObjCmdProc(@Hello_Cmd), nil, nil);
    Tcl_CreateObjCommand(interp, 'square', Tcl_ObjCmdProc(@Square_Cmd), nil,
                         nil);
    Tclfpexample_Init := TCL_OK;
end;

exports
    Tclfpexample_Init;

end.

Discussion

arjen - 2016-10-06 06:40:48

You could use the technique I described in Interfacing with the Tcl C API from Fortran to generate the "complete" interface to the Tcl C functions.

MJ - 2017-08-05

Note that the code above doesn't actually use the stubs mechanism and therefore can only load in a specific Tcl version. To actually use the stubs mechanism, the code below demonstrates initializing the stubs pointer and using it to call the first stubbed Tcl procedure. As demonstrated by the code below.

A full stubs enabled wrapper for Tcl 8.6 can be found on https://github.com/mpcjanssen/fpc-tcl . Note that tcltypes.inc file is incomplete and assumes most types are opaque pointers. I will refine it as needed.

% load tclfpexample.dll
% package require test
0.1

The code is split in a tcl unit file and a sample extension. The tcl unit is available from the github link above.

tclsampleext.pas

library tclsampleext;

{$mode objfpc}{$H+}

uses {$IFDEF UNIX} {$IFDEF UseCThreads}
  cthreads, {$ENDIF} {$ENDIF}
  Classes,
  SysUtils,
  ctypes,
  tcl;

type
  TMailingListRecord = record
    FirstName: string;
  end;
  PMailingListRecord = ^TMailingListRecord;

  procedure Square_Del_Cmd(clientData: ClientData); cdecl;
  begin
    WriteLn('Clearing Square clientData which had firstname: ' + PMailingListRecord(clientData)^.FirstName);
    Dispose(PMailingListRecord(clientData));
  end;

  function Square_Cmd(clientData: ClientData; interp: PTcl_Interp;
    objc: cint; objv: PPTcl_Obj): cint; cdecl;
  var
    i: cint;
    ml: TMailingListRecord;
  begin
    ml := PMailingListRecord(clientData)^;
    WriteLn('FirstName in ClientData ' + ml.FirstName);
    if objc <> 2 then
    begin
      Tcl_WrongNumArgs(interp, 1, objv, 'value');
      Exit(TCL_ERROR);
    end;
    WriteLn('objv[1]:' + Tcl_GetString(objv[1]));
    if Tcl_GetIntFromObj(interp, objv[1], @i) <> TCL_OK then
    begin
      Exit(TCL_ERROR);
    end;
    Tcl_SetObjResult(interp, Tcl_NewIntObj(i * i));
    Result := TCL_OK;
  end;

  function Tclsampleext_Init(interp: PTcl_Interp): cint; cdecl;
  var
    ptr: PMailingListRecord;
  begin
    ptr := New(PMailingListRecord);
    ptr^.FirstName := 'Mark';
    Tcl_InitStubs(interp, '8.5', 0);
    Tcl_PkgProvideEx(interp, 'test', '0.1', nil);
    Tcl_CreateObjCommand(interp, 'square', @Square_Cmd, ptr, @Square_Del_Cmd);
    Result := TCL_OK;
  end;

exports Tclsampleext_Init;

end.

And the test script:

testsample.tcl

load tclsampleext.dll

puts [package require test]

puts [square 12]

# Test cmd_del_proc callback

rename square {}

Which gives the outpu:

% tclkitsh testsample.tcl
0.1
FirstName in ClientData Mark
objv[1]:12
144
Clearing Square clientData which had firstname: Mark