From: David McClain
Subject: Safe DLL Linkage in ACL/6.1
Date: 
Message-ID: <rrjJ7.1058$cG1.238625@news.uswest.net>
DLL linkage in ACL 6.1 under Windows OS is inherently unsafe from the
standpoint that the routine you actually call depends on the loading history
of DLL's, globally, irrespective of whatever packaging you provide for your
foreign interface code. The ACL technique uses a hashtable underneath so
that the results are independent of the package in which you define your
interface routines.

The documentation states that a warning is issued in the event that foreign
routines get redirected to a new DLL when that new DLL is loaded. But this
is not the case, at least for Windows/NT 4.0. The redirection happens
quietly.

If you accidentally happen to load a DLL that redefines a named entry point,
you will silently be redirected to use that new routine, even if it happens
to have nowhere near the same interface as some other routine you had
planned on using, which was located in a DLL loaded earlier.

The following code rectifies that situation by defining a new macro for
foreign function interfaces, named directed-def-foreign-call. This takes a
keyword argument :module which should be followed by the string name of the
DLL in which your foreign code resides. It also requires a 2-element list
for the foreign function name: the first element is a symbol naming your
desired Lisp interface, the second is a string argument literally naming the
entry point in the export list of the DLL.

This macro depends on the existence of a foreign routine named
LispCallIndirect as provided below. The macro builds a secret call to this
interface and passes the actual routine address as the first argument. This
foreign routine pops that address from the stack and redirects the call to
the intended foreign routine. Hence, this should be transparent to the
entire system, except for a few cycles of additional overhead in the foreign
call. You do not need to (load <dllname>) before using these directed
interface functions. If the DLL is not already present in the process image,
then it will be loaded when the macro defines your interface routine.

LISP Code:
----------------------------------------------------------------------------
-----------
(load "kernel32.dll")

(ff:def-foreign-call (getmodulehandle "GetModuleHandleA")
    ((name (* :char)))
  :strings-convert t
  :returning  :long
  :convention :stdcall)

(ff:def-foreign-call (loadLibrary "LoadLibraryA")
    ((name (* :char)))
  :strings-convert t
  :returning :long
  :convention :stdcall)

(ff:def-foreign-call (getProcAddress "GetProcAddress")
    ((module-handle :long)
     (proc-name     (* :char)))
  :strings-convert t
  :returning :long
  :convention :stdcall)


(load "lispcom.dll")  ;; contains the LispCallIndirect() function (see
below...)

(defun directed-determine-foreign-address (module-name c-proc-name)
  (let ((mh (let ((mhg (getModuleHandle module-name)))
              (if (zerop mhg)
                  (loadLibrary module-name)
                mhg))))
    (if (zerop mh)
        (error "Can't locate module named ~A." module-name)
      (let ((pa (getProcAddress mh c-proc-name)))
        (if (zerop pa)
            (error "Can't locate entry point named ~A in module ~A."
c-proc-name module-name)
          pa))
      )))

(defmacro directed-def-foreign-call (name args &rest keys)
  (let* ((c-proc-name   (second name))
         (user-name     (first name))
         (internal-name (gensym))
         (module-name   (getf keys :module))
         (pa            (directed-determine-foreign-address module-name
c-proc-name))
         (argnames      (mapcar #'first args)))
    (remf keys :module)
    `(progn
       (ff:def-foreign-call (,internal-name "LispCallIndirect")
           ((module-proc-address :long)
            ,@args)
         ,@keys)
       (defun ,user-name ,argnames
         (,internal-name ,pa ,@argnames))
       )))

---------------------------------------------------------------------
C Code

 extern "C" __declspec(dllexport) long LispCallIndirect()


    // routine starts out pushing ebp, ebx, esi, and edi -- we undo that
here...
  _asm {
   pop edi  // pop saved regs
   pop esi
   pop ebx
   mov esp,ebp
   pop ebp  // pop frame ptr
   pop eax  // pop return address
   pop ecx  // pop called address
   push eax // push return address
   jmp ecx
  }
  return 0;  // compiler dummy
 }

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
 return TRUE;
}

----------------------------------------------------------------------------
--
As a sample of use, I constructed two DLL's each containing a function named
doit(). The first DLL returns the value 15 when doit() is called. The second
DLL returns 16 when doit() is called.

Here is a transcript of interaction with these two routines, and one defined
as a directed DLL interface, named doit1.

CG-USER(8): (directed-def-foreign-call (doit1 "doit")
                           ()
                           :module "diddly1.dll"
                           :returning :long)
DOIT1
CG-USER(9): (ff:def-foreign-call doit ()
  :returning :long)
Warning: String arguments passed to `DOIT' will be converted because
:STRINGS-CONVERT is assumed true.The
         with-native-string macro can be used for explicit string
conversions around the foreign calls.
         This warning is suppressed when :STRINGS-CONVERT is specified in
the def-foreign-call.
DOIT
CG-USER(10): (load "diddly1.dll")
; Foreign loading diddly1.dll.
T
CG-USER(11): (doit)
15
CG-USER(12): (doit1)
15
CG-USER(13): (load "diddly2.dll")
; Foreign loading diddly2.dll.
T
CG-USER(14): (doit)
16
CG-USER(15): (doit1)
15
CG-USER(16):

Notice, that while (doit) got redirected upon loading "diddly2.dll", without
warning, the routine (doit1) continues to return the correct value.

- David McClain, Sr. Scientist, Raytheon Systems Co., Tucson, AZ