Building and Using an Allegro Common Lisp Based DLL

Allegro Common Lisp developers can build DLL's that they and other C/C++ programmers may use in the same manner as they use any other Windows function library. Below is an example that illustrates the steps necessary to construct and deploy an ACL based DLL.


Sections:

Using An Example ACL Based DLL: fact.dll
Building the Example DLL: fact.dll
Building the Companion DXL File: fact.dxl
Building Your Own Lisp Based DLL
Deploying Your Lisp Based DLL
LNKACL DLL Exported Routines


Using An Example ACL Based DLL: fact.dll

The fact.dll example provides a function that computes a factorial (1 x 2 x 3 x ... x (n-1) x n). Since Lisp supports "bignum" arithmetic, it is easy to write such a factorial function in Lisp. Without Lisp, A C/C++ programmer must address integer overflow for larger input arguments.

The example fact library exports two functions:

int initialize_factorial()

Parameters

none

Return Value

1                                         initialization successful
0                                         initialization failure

int factorial ( unsigned int x, char *result, int string_size )

Parameters

x                                           the factorial argument
result                                     a string buffer large enough to hold the result
string_size                              the string buffer size

Remarks

The factorial() function returns the answer as a string to avoid C integer overflow problems.

Here is a file called ftest.c that uses the fact library. Note that it is a console application; you could use the same functionality in a Windows application:

#include <stdio.h>

void usage();

main( int argc, char *argv[] )
{
    int i, n, ret;
    char result_string[4096];

    i f (argc != 2)
    {
        usage();
        exit( 1 );
    }
    if (strlen( argv[1] ) > 3 )
    {
        usage();
        exit( 1 );
    }
    for (i = 0; *(argv[1] + i); i++)
    {
        if (!isdigit( *(argv[1] + i) ))
        {
            usage();
            exit( 1 );
        }
    }
    sscanf( argv[1], "%d", &n );
    if (initialize_factorial() != 1)
    {
        fprintf(stderr, "factorial startup error" );
        exit( 1 );
    }
    if ((ret = factorial( n, result_string, 4096 )) != 1)
    {
        fprintf(stderr, "factorial internal error" );
        exit( 1 );
    }
    printf( "%s", result_string );
    exit( 0 );
}

void
usage()
{
    fprintf( stderr, "usage: ftest n, where n is an integer between 0 and 999" );
}
                          

Here is the compilation, linking, and a sample run:

cl -nologo -Od -c -Zi -I -W3 -G3 ftest.c -Foftest.obj

link -nologo -debug -out:ftest.exe ftest.obj fact.lib

ftest 123
12146304367025329675766243241881295855454217088483382315328918161829235892362167
66883115696061264020217073583522129404778259109157041165147218602951990626164673
0733907419814952960000000000000000000000000000


Building the Example DLL: fact.dll

Here is the fact.c file containing the source code for the fact DLL:

#include <windows.h>
#include <string.h>

#define Dllexport _declspec(dllexport)

int lisp_initialized = 0;

int (*lisp_factorial)(int, char *, int) = 0;

/* returns 0 if lisp previously initialized
returns 1 if initialization successful
returns -1 if initialization failed
*/
Dllexport int initialize_factorial()
{
    if (!lisp_initialized)
   {
        if (InitializeLisp( "fact.dxl", 0, 0 ) == 0)
        {  
            return -1;
        }
/* don't return until lisp can process commands */
        if (RemoteCommand( "(initialize-factorial)", 1 ) != 1)
            return -1;
        lisp_initialized = 1;
        return 1;
    } else
    {
        return 0;
    }
}

/* Lisp calls this to set factorial callback address */
Dllexport void set_factorial_callback( int (*fcb) (int, char *, int) )
{
    lisp_factorial = fcb;
}

/* users of the factorial library call this */
/* returns 1 if successful; 0 otherwise */
Dllexport int factorial ( unsigned int x, char *result, int string_size )
{
    if (!lisp_initialized || !lisp_factorial) {
        return -1;
    }
    switch ( (*lisp_factorial) ( x, result, string_size ))
    {
        case 1:
            return 1;
        case 0:
        default:
            *result = '\0';
            return 0;
    }
}

/* Lisp calls this to fill result string */
int Dllexport
copy_factorial_result( char *str1, char *str2, int n )
{
    strncpy( str1, str2, n - 1 );
    *(str2 + n) = '\0';
}

The above code uses two functions exported by the lnkacl library provided by Franz for building Lisp base DLL's. The relevant source code lines are:

if (InitializeLisp( "fact.dxl", 0, 0 ) == 0)

and

if (RemoteCommand( "(initialize-factorial)", 1 ) != 1)

These functions are documented in the LNKACL DLL Exported Routines section.

Here are the compilation and linking commands:

cl -nologo -Od -c -Zi -I -W3 -G3 fact.c -Fofact.obj
link -nologo -dll -debug -out:fact.dll fact.obj lnkacl.lib

Note that lnkacl.lib file (provided by Franz) must be either in the current directory or in a directory named in the "lib" environmental variable.


Building the Companion DXL File: fact.dxl

Here is the fact.cl file containing the custom fact.dxl code:

(in-package :user)

(defun factorial (x)            ; assumes caller will trap errors
    (cond
        ((= x 0) 1)
        (t (* x (factorial (1- x))))))

(eval-when (load eval)
    (load "fact.dll"))

(ff:def-foreign-call set_factorial_callback (address))

(ff:def-foreign-call copy_factorial_result
    ((str1 :unsigned-long) (str2 (* :char)) (n :int)))

(ff:defun-c-callable (factorial-callback :c)
      ((arg :unsigned-long) (string :unsigned-long) (length :unsigned-long))
      :long
    (handler-case
        (progn
            (copy_factorial_result string
                (format nil "~s" (factorial arg))
                length)
            1) ;; return 1 if successful
        (serious-condition (condition)
            0)) ;; return 0 if unsuccessful
)

(defun initialize-factorial ()
    (mp:process-run-function
        "Factorial"
        #'(lambda ()
            (let ((ct 0))
                (loop
                    (mp:process-wait
                        "Wait for immigrant"
                        #'(lambda ()
                            (> (mp::thread-test :waiting-threads nil)
                                0)))
                    (incf ct)
                    (mp:make-immigrant-process
                        (format nil "CB-~d" ct))))))
    (set_factorial_callback (ff:register-function #'factorial-callback nil t)))

These Lisp forms are used to create the fact.dxl file:

(load "lnk.fasl") ;; provided by Franz
(compile-file "fact.cl")
(load "fact.fasl")
(sys::resize-areas :old 512000 :verbose t :global-gc t :pack-areas nil)
(dumplisp :name "fact.dxl")

The above commands are entered into a non-IDE Allegro Common Lisp session.


Building Your Own Lisp Based DLL

Your C/C++ code and Lisp code should include the following:

1. Your C/C++ code must initialize Lisp by calling InitializeLisp().

You can do this with an exported initialization function as in the above example, or you can transparently wrap the initialization steps within your DLL's exported functions.

2. Your C/C++ code must call a custom Lisp initialization function.

After calling InitializeLisp(), but before calling any of your other Lisp code, you must call a custom initialization Lisp function using RemoteCommand(). The custom initialization function is described in steps 6 and 7.

3. Your C/C++ code must provide at least one exported function to setup entry points.

Your Lisp initialization function calls this routine to fill in addresses at runtime that your C/C++ code uses to invoke Lisp based functionality. In the example above, the set_factorial_callback() function provided this functionality.

4. Provide C/C++ wrappers for the Lisp functionality you wish to export.

The factorial() function in the above example illustrates how to do this.

5. You must load the lnkacl.fasl file provided by Franz into your custom Lisp image.

Remember to name the image so it matches your InitializeLisp() argument.

6. Your custom Lisp initialization code must initiate a Lisp process that waits for remote callback invocation.

Here is example code (it is similar to the factorial example, above):

(mp:process-run-function
        "put a useful name here"
        #'(lambda ()
            (let ((ct 0))
                (loop
                    (mp:process-wait
                        "Wait for immigrant"
                        #'(lambda ()
                            (> (mp::thread-test :waiting-threads nil)
                                0)))
                    (incf ct)
                    (mp:make-immigrant-process
                        (format nil "CB-~d" ct))))))

7. Your custom Lisp initialization code must call the exported function described in step 3 to setup callback entry points.

You will use 'load to load your DLL, 'ff:def-foreign-call to make the exported function available, ff:defun-c-callable to define your exportable Lisp functionality, and 'ff:register-function to generate an address to provide to the function defined in step 3. If your C/C++ code process return values, you will most likely want to use the 'ff:register-function 'convert optional argument.


Deploying Your Lisp Based DLL

To allow building an application that uses a custom Lisp based DLL:

You must provide the .lib file generated when you built your DLL.

The file must reside in a directory included in the LIB environmental variable, or it must reside in the directory where the build occurs. Remember that you must export any functions you wish to make available to applications using your DLL.

To allow a generated application to run:

You must provide lnkacl.dll (provided by Franz), your custom DLL, and your custom Lisp image file.

These files can reside in any of the following directories:

The directory from which the calling application loaded
The current directory
The windows system directory
The windows directory
One of the PATH environmental variable directories

For Windows 95 usage, you must provide the aclxxx.vxd file (provided by Franz)

This file should be placed in the windows\system directory.

In addition, you may have to provide any DLL's required by your C/C++ compiler.


LNKACL DLL Exported Routines

int InitializeLisp( char *image_file_name, char *sys_dir, int make_console )

Parameters

image_file_name                      Lisp image filename
sys_dir                                     directory Lisp will use as sys: for logical pathnames
                                       (if 0, the application's working directory will be used)
make_console                         1, open debugging console
                                              0, don't open debugging console

Return Value

1                                              found image file
0                                              couldn't find image file

See Also                                     TerminateLisp()

Remarks

The image file can be a complete path or just a file name. If just a file name, the following directories are searched, in order:

The directory from which the calling application loaded
The current directory
The windows system directory
The windows directory
The PATH environmental variable directories

The debugging console is useful during development - you can view initialization messages, enter Lisp expressions, and manage Lisp debugging activities.

Note that this routine returns asynchronously. The Lisp initialization takes some time to complete. Use a subsequent synchronous
RemoteCommand() call if you desire a synchronous startup procedure or if you wish to verify that the Lisp startup procedure has completed successfully.


int RemoteCommand( char *command_buf, int wait_for_message )

Parameters

command_buf                                       string to be processed by eval in Lisp thread
wait_for_message                                  0, return asynchronously
                                                             1, wait for completion before returning

Return Value

1                                                           wait_for_message = 0 or successful completion
0                                                           Lisp error occurred during command.
-1                                                          Lisp thread no longer alive

Remarks

Note that Windows messages are processed while waiting for successful command completion.

void TerminateLisp()

Remarks

Terminates Lisp environment and closes debugging console if necessary.

InitializeLisp() may be called to restart the Lisp environment after this routine has been called.


unsigned int GetLispThread()

Return Value

The Lisp thread handle is returned.