This chapter introduces the basics of the Win32 API, demonstrates how it can be called from Lisp, and shows how Lisp can be used to create a powerful Win32 programming environment optimized for a specific application domain. Any such Lisp environment exposing every option in the Win32 API must be at least as complex as the Win32 API. However, it is hard to imagine a given application domain needing every option of every function in the Win32 API. Extending Lisp's syntax in a domain-specific manner hides those parts of the Win32 API superfluous to the domain. (Most likely the majority of the Win32 API will be hidden.) The programmer then deals with and thinks about only those pieces of the API needed for the task at hand. Pieces of the Win32 API needed later are easily exposed at any time.
A single environment is not easily optimized for both simple and complex applications. The methods outlined in this chapter can be used to construct environments, from simple to complex, appropriate for a range of application domains.
The beginning sections of the chapter introduce concepts and ideas leading to the penultimate section, which concludes with a "Hello" program:
(require "win-header")
(in-package :wh)
(export '(initialize-appendix-b))
(DefMsgHandler appendix-b-proc MsgMap ()
(WM_PAINT
(text-out "Hello from Lisp!" 0 0 :color (RGB 0 0 255))
0))
(defun initialize-appendix-b ()
(DefActiveTopWindow "Hello" 'appendix-b-proc
:style ws_overlappedwindow :width 150 :height 200 :title "Hello Program"))
Lispworks for Windows version 4.2.7 is used. (The Personal Edition is available for free download.) No attempt is made to accomodate other Lisps and no thought is given to cross-platform compatibility. These ideas were discussed when outlining this document and the current approach decided upon because:
Anyone programming for the Win32 API should be familiar with Programming Windows, The Definitive Guide to the Win32 API by Charles Petzold, or a comparable book.
The code presented here has been tested only with Lispworks for Windows 4.2.7 Professional Edition under Windows XP.
One concern some may have is the perception that Lisp executables are large. Microsoft operating systems are written in C and C++. The supporting libraries for C/C++ programs come with the operating system. Lisp run-time support looks large only because it does not come with the OS. It is possible to ship Lisp programs in delivery mode, especially with Lispworks for Windows which requires no royalties for this, and it is possible to ship the run-time once and then ship small, compiled programs as they become available.
If the requirement is to create small programs suitable for public download in large volumes, the fact that Lisp runtime support is not loaded on most computers is a limitation. However, Lispworks for Windows allows aggressive pruning during the delivery stage and programs can sometimes be reduced below two megabytes, which is suitable for many download situations. Situations requiring the full Lisp run-time are most likely large applications where it makes sense to deliver the runtime on CD, or via a large one-time download, and later deliver individually-compiled components which are loaded during program or component initialization.
It is my understanding that the lower limit on delivered program size is not inherent to Lisp as a language but only a result of current market demand. As market demand grows for truly small stand-alone executable programs written in Lisp, vendors will have incentive to spend time developing better pruners and shakers. (Lisp starts with a lot and removes what's not needed to produce a deliverable while C/C++ starts with a little and adds what's needed during the link phase. A freshly-run Lisp begins with a REPL, a read-eval-print loop, with the full power of Lisp available interactively and ready for use.)
Seeing the gory details of Lispwork's Foreign Language Interface, or FLI, may seem strange at first. The OS is written in C/C++, so OS calls from C/C++ are not foreign calls, just as Lisp functions called from Lisp functions are not foreign calls.
to determine which format is in use. Win32 functions taking or
returning characters or strings come in two flavors: 1) those ending
in A for ASCII characters and 2) those ending in W for wide Unicode
characters. This external-format function is useful primarily when
calling
(defun external-format ()
(if (string= (software-type) "Windows NT")
:unicode
:ascii)
fli:with-foreign-string
, part of the Lispworks
foreign function interface. When defining Win32 functions in the
Foreign Function Interface, or FLI, the presence of the keyword :dbcs
indicates that the function has both a single-byte and a double-byte
version. When :dbcs is present, Lispworks appends an "A" to the
function name in single-byte Windows 95 and a "W" in double-byte
Windows NT/2000/XP. (I wrote and tested the example program (see
Appendix A) under Windows XP.) Without :dbcs, Lispworks leaves the
foreign function name unchanged.
One FLI definition for the Win32 TextOut function is:
which is equivalent to:
(fli:define-foreign-function
(TextOut "TextOut" :dbcs :calling-convention :stdcall)
((HDC (:unsigned :long)) (nXStart (:unsigned :int)) (nYStart (:unsigned :int))
(lpString :pointer) (cbString (:unsigned :int)))
:result-type (:unsigned :long))
(fli:define-foreign-function
(TextOut "TextOutW" :calling-convention :stdcall)
((HDC (:unsigned :long)) (nXStart (:unsigned :int)) (nYStart (:unsigned :int))
(lpString :pointer) (cbString (:unsigned :int)))
:result-type (:unsigned :long))
under NT/2000/XP (the second example would use "TextOutA" under 95).
To demonstrate this, let's use a simple FLI definition which is easy to call interactively for testing purposes. We are trying only to see if a given Win32 function is known to the OS. In the following REPL interaction, the return result is important only when the Lisp restart handler is invoked. When the restart handler is not invoked, the Win32 function was found, loaded and called. Trying to call a Win32 function which the FLI cannot find results in an invocation of the restart handler. (The correct define-foreign-function definition for textout can be found in Appendix A
CL-USER 9 > (fli:define-foreign-function
(TextOut-1 "TextOut" :dbcs :calling-convention :stdcall)
() :result-type :int)
TEXTOUT-1
CL-USER 10 > (textout-1)
1
CL-USER 11 > (fli:define-foreign-function
(TextOut-2 "TextOut" :dbcs :calling-convention :stdcall)
() :result-type :int)
TEXTOUT-2
CL-USER 12 > (textout-2)
0
The TextOut
function was found both times. This
shows that a given Win32 function can be named in more than one FLI
definition. This technique is sometimes useful when more than one Lisp
datatype can satisfy the requirements for a parameter of the Win32
function.
CL-USER 13 > (fli:define-foreign-function
(TextOut-3 "TextOutW" :dbcs :calling-convention :stdcall)
() :result-type :int)
TEXTOUT-3
CL-USER 14 > (textout-3)
Error: Foreign function TEXTOUT-3 trying to call to unresolved
external function "TextOutWW".
1 (abort) Return to level 0.
2 Return to top-level loop.
3 Return from multiprocessing.
Type :b for backtrace, :c
I elided a warning Lispworks gives after multiple definitions of a
foreign function when the previous definition differs in its use of
the :dbcs keyword from the current definition's use. After
CL-USER 14, Lispworks complains about "TextOutWW", which shows that
using the :dbcs keyword causes Lispworks to append a 'W' to the
foreign function's name, although the existence of the foreign
function itself is not verified until an actual call is
made. TextOut-5 verifies the existence of the Win32 function TextOutA,
which is the ASCII version of TextOut.
I have seen strange character sets in titles and other places and have been able to resolve those problems by ensuring I made consistent use of the :dbcs and/or W/A declarations.
Edi Weitz asked if the :dbcs keyword decides at compile time or at run time which function to call, the ...A or the ...W. I wrote the following program:
(in-package :cl-user)
(fli:define-foreign-function
(MessageBox "MessageBox" :dbcs :calling-convention :stdcall)
((hwnd (:unsigned :long)) (text (:unsigned :long)) (title (:unsigned :long)) (flags (:unsigned :long)))
:result-type (:unsigned :long))
(defun external-format ()
(if (string= (software-type) "Windows NT")
:unicode
:ascii))
(defun display-format-used ()
(fli:with-foreign-string
(unicode-p u-ec u-bc :external-format (external-format)) "Unicode"
(fli:with-foreign-string
(ascii-p a-ec a-bc :external-format (external-format)) "Ascii"
(fli:with-foreign-string
(title-p t-ec t-bc :external-format (external-format)) "External Format"
(if (eq (external-format) :unicode)
(messagebox 0 (fli:pointer-address unicode-p)
(fli:pointer-address title-p) 0)
(messagebox 0 (fli:pointer-address ascii-p)
(fli:pointer-address title-p) 0))))))
(compile 'external-format)
(compile 'display-format-used)
(deliver 'display-format-used "dbcs-run" 5)
(quit)
; We then have dbcs-run.exe. When run on Windows XP, dbcs-run pops up a messagebox
; displaying "Unicode". The same dbcs-run.exe file, ftp'd to a Macintosh running OS 9 with
; Virtual PC running Windows 98, pops up a message box displaying "Ascii".
#include
d in the program. The header files
contain the definitions of constants, structures, and functions
comprising the API. These definitions must be available to the Lisp
program. I find it most straightforward to do this conversion by
hand. Although there are automated methods, doing it manually does
not take long on a per-function basis.
In C/C++, the #define
s exist in the preprocessor.
Only those define
s used by the program are included in
the object code. With Lisp, the defconstants
are all
loaded into the Lisp image, whether or not they are subsequently
used. I do not know a clean solution for this issue.
In the meantime, I make a base, or core, win-header.lisp and use other .lisp files, grouped by functionality, for less-frequently-used definitions, loading those .lisp files when I need them.
Lisp does not know or care about the difference between an HINSTANCE and an HMENU. They both are simply 32-bit values. Lisp pays attention to these values at two different points in time: 1) when moving Lisp data to a foreign field and 2) when moving the foreign data to Lisp. Lispworks attempts coercion at those points and conditions result when incorrect attempts are made to do conversions like stuffing a negative value into an unsigned field. If more hints about type are given to Lisp, such as declaring a foreign field to be of type :pointer, Lisp will complain when trying to stuff zero into the pointer. That is not handy if one is trying to pass a null pointer to the OS. Thus, I find it easier to call most parameters long, although I bend that rule from time to time.
Lispworks FLI pointers are actually a Lisp structure containing an
address retrieved, or unboxed, by fli:pointer-address. When passing a
pointer value to the OS, for example when passing the address of a
RECT
to GetClientRect
, there are two steps
that need to happen: 1) allocate the foreign structure and 2) pass the
address of that allocated structure to the OS. Most of the time these
allocations are best handled with
fli:with-dynamic-foreign-objects
enclosing calls to
fli:allocate-dynamic-foreign-object
because one doesn't
have to worry about deallocations. I pass the address of the allocated
structure using fli:pointer-address
(unboxing the pointer
value) and define the field in the foreign function's parameter list
as an unsigned long.
The FLI allows things to be defined such that Lispworks will try automatic coercion (unboxing). Try defining the parameter type as :pointer. However, Lispworks complains when trying to pass a NULL pointer, although I did not try creating a FLI pointer with address zero. The approach I chose, calling pointers unsigned longs, is clear to me and works well in both directions (OS->Lisp, Lisp->OS). This may simply be a result of my current lack of complete understanding and there may be a better way.
On occasion is it helpful to define C arrays inside C structures, in
particular in sPAINTSTRUCT
. This works but I don't like
my current method of obtaining the address of structure members or
array entries. I find myself counting byte offsets by hand and using
something like:
(defun interior-copy (to-struct-ptr byte-offset src-ptr)
(let ((ptr (fli:make-pointer
:address (fli:pointer-address to-struct-ptr :type :char))))
(fli:incf-pointer ptr byte-offset)
(wcscpy (fli:pointer-address ptr) (fli:pointer-address src-ptr))))
where wcscpy, the wide-character version of strcpy, is defined
through the FLI. I hope there's a better way to do this and that
someone quickly teaches me. I haven't worked enough with different
OSes and Lispworks to know the best way to choose strcpy vs. wcscpy,
other than to use (software-type) to decide which to call. (Or use
(external-format), defined in Appendix A.)
Although the data types defined to Lisp are kept a minimum, it is
very useful for documentation purposes to mimic the typedef names used
in the C/C++ header files. Thus fli:define-c-typedef
is
used to define BOOL, DWORD, HANDLE, HDC, and other similar Win32 data
types.
Many OS-specific constants must be made available to the Lisp program:
(defconstant CW_USEDEFAULT #x80000000)
(defconstant IDC_ARROW 32512)
(defconstant SW_SHOW 5)
(defconstant WM_CLOSE #x00000010)
(defconstant WM_DESTROY #x00000002)
These constants are given by name, without values, in the MSDN
documentation. The Lisp program needs not only the name but also the
value. An easy way to find the necessary values is to grep through the
VC98/Include directory. Visual Studio contains a "find in files"
function on its toolbar which allows this kind of search. Kenny Tilton
says, "What I did was grab any VC++ project that builds (the NeHe
OpenGL site is full of VC++ projects (see OpenGL tutorials in sidebar
to left of the page at http:///nehe.gamedev.net) which
built without a problem for me) and then right-click on the symbol I
was curious about. (Of > course first you have to find a reference
All the Win32 calls I've seen so far are
This wndproc function is the message dispatcher. The OS calls
wndproc once for every message sent to the program. Wndproc is
responsible for understanding the message and calling the appropriate
function.
The #. reader macro returns the value of The example Win32 Lisp program in Appendix A
may be run either from the Lispworks IDE or from console mode, such as
ILISP in Emacs. If Using multiprocessing has not proven so nice for me under ILISP. I
love the ILISP and Emacs environment. The Lispworks IDE is very nice,
and I keep a copy of it going for certain tasks such as finding online
manuals and using debug tools such as the inspector. For editing and
most running, though, I prefer Emacs and ILISP. However, I have not
learned how to view multiple processes under ILISP, nor do I know how
to switch between them. When I use multiprocessing with ILISP, it
appears to me that any thread with a condition grabs *standard-output*
and *standard-input*. I don't know how to switch back to the other
thread. This is enough of a problem that I don't use multiprocessing
under ILISP and when I need or want the interactive debug capabilities
possible with multiprocessing, or need multiprocessing in any form, I
switch to the Lispworks IDE.
When running under the IDE, Lispworks provides the message pump.
When running under Emacs/ILISP (or in console mode, as would happen in
a delivered application), the Lisp program itself must provide the
message pump.
Thus in the example program in Appendix A, the function
The program in Appendix A makes a call to register-class when the file is
loaded. The call needs to be made only once and so I make the call
at the top-level:
Lisp is designed to allow programs to run for years at a time, with
careful management, and to allow the programs to be maintained, with
bugs fixed, new functions defined, and CLOS objects redefined, during
that time.
However, even in a pure Win32 environment it is reasonable to want
to use CAPI's features quickly to generate advanced GUI programs
without having to recreate every wheel. It is possible to use
Win32-specific features from within a CAPI program.
Note the connections from image-pane to pane-1 and from image-pane to
draw-image, and that pane-1 is contained in the CAPI window.
Draw-image gets called when it is time to ... well, when it is time to
draw the image!
Certainly calls to Win32 functions which don't require handles to
windows or other interaction with the CAPI environment work just fine.
Many good programs use primarily local variables. If one wishes to
use the multiprocessing environment to operate upon the program from
the Lispworks IDE while the Windows program is running, it is
important to have access to symbols and variables for the window
handles and other Windows resources. If Make a def file, such as avicap32.def, named after the desired DLL:
Windows created using functions in DLLs can be given a CAPI
window as a parent, as previously shown. The def file may
or may not be required, depending upon what functions are
desired. Maybe :dbcs should be used here, eliminating the need for the
hard-coded 'W' in the foreign function name.
Objects with dynamic extent are declared local at the
beginning of a C++ function and the object's destructor is called when
the function returns and the object goes out of scope. The
corresponding Lisp idiom is the use of a In C++, objects with indefinite extent must have their destructor
called explicity, with Lisp is garbage collected, which means that Lisp is responsible for
freeing the object's memory. However, that may not happen for a very
long time after the last reference to the object has disappeared. The
garbage collector runs only as memory fills or when it is explicity
called. If an object holds an acquired resource, almost always there
is a proper time to release the resource and not releasing it at
that time leads to resource exhaustion.
Lisp is not responsible for acquired resources, such as window
handles, which the programmer acquired with explicit Lisp code. The
programmer must define a function, something like Another issue with the Win32/Lisp environment concerns the GC,
which is free to move Lisp data. One cannot give the OS the address of
Lisp data which may be moved by the GC. Any data given to the OS
should be allocated through the FLI, which is responsible for making
the data immovable.
Win32 API programming cries out for new languages. It is a
very powerful and flexible API but in a given application context,
only certain subsets are used and they are used in repetitive
fashions. This does not mean that the APIs should be redefined, were
that possible. What works for one application may not work for the
next. There probably are some language extensions that will be used
in nearly all Win32 programs. Other extensions will apply only to
certain applications.
One beauty of Lisp is that the programmer can define a new
extension at any time. See the Common Lisp
Cookbook's chapter on macros. I also recommend Paul Graham's On Lisp for learning to write
macros and a whole lot more.
When writing code, notice when the same pattern is typed over and
over. Then think, sooner rather than later, "it's time for a macro or
a function." Notice the repetitive coding even when you're writing
macros. Macros can be built upon macros, and macros can generate
macros.
Knowing whether to choose a macro or a function is partly a
function of code bloat. Macros are evaluated in place and cause new
code to be created, where functions do not. The advantage of macros
is that they can create closures, capturing variable values present in
the environment when the macro is expanded, thus eliminating much of
the need for simple objects. If the macro creates If macros are not used in this way, then functions that would have
been created by the macro must have some form of object look-up code
to retrieve window handles, captions, and other resources specific to
the object in question.
Be aware that functions can be declaimed inline and can be passed
as parameters, while macros cannot.
Many of the macros defined in On Lisp are very useful in
Win32 programming. I use the symbol creation macros extensively.
One set of needed macros make using the Foreign Language Interface
easier, more compact, and more readable. I have a macro
Another set of macros is useful for defining the Windows message
handler functions. I prefer to create a CLOS class for windows
messages and let the CLOS method dispatcher find the proper function
for each message. This allows message handlers to be inherited and
more-specific handlers defined for selected messages in the derived
class. CLOS handles this nicely. I have a Look at Appendix B. Compare it to Appendix A. Of course, an extended version of
the header-file portion of Appendix A is used in Appendix B but not
shown there. All but a little of the program in Appendix A has been
reduced to a library which is invoked in Appendix B. All of the
application-specific information is contained in the very small
program in Appendix B.
The program in Appendix C uses these
macros to define a window including radio buttons, pushbuttons, a
check box, text drawn on the background window, and a listview with
columns.
Lisp provides an interactive and rich programming environment. The
judicious use of macros to create language extensions in Lisp
concentrates application-specific information into small local areas
of the overall system. This simplifies the effort of understanding
the application, increases the reliability of the application, reduces
maintenance time, and increases the reliability of maintenance
changes. Lisp is designed to make this easy.
It may be advantageous to open a separate window with the
program source code visible while reading the text.
Copyright © 2002-2007 The Common Lisp Cookbook Project
FLI Data Structures
I usually define the structure and a typedef for it:
; PAINTSTRUCT
(fli:define-c-struct sPAINTSTRUCT
(HDC hdc)
(fErase bool)
(rcPaint-x uint)
(rcPaint-y uint)
(rcPaint-width uint)
(rcPaint-height uint)
(fRestore bool)
(fIncUpdate bool)
(rgbReserved (:c-array wBYTE 32)))
(fli:define-c-typedef PAINTSTRUCT sPAINTSTRUCT)
and then can do something like:
(fli:with-dynamic-foreign-objects ()
(let ((ps-ptr (fli:allocate-dynamic-foreign-object :type 'paintstruct)))
(format t "~&Pointer value: ~a" (fli:pointer-address ps-ptr))))
although I'm not clear on why the typedef is valuable. Lisp is
not C and in Lisp the typedef does not save me from typing
struct sPAINTSTRUCT
, for example. I think the typedefs
are superfluous and I probably will stop using them.
FLI Functions
It is very easy to define OS calls in the FLI. I start with the API
definition in the OS documentation. If Visual C++ is available, the
MSDN documentation is probably loaded on the machine. The
documentation is available on the MSDN website. I go to the Win32
documentation page for the desired function and do a simple
translation:
; LoadCursor
(fli:define-foreign-function
(LoadCursor "LoadCursor" :dbcs :calling-convention :stdcall)
((hInstance handle) (param ulong))
:result-type handle)
:calling-convention :stdcall
. If I know the function
includes a text parameter, I include the :dbcs keyword. If I don't
know, I try it without :dbcs. The actual function called in this
example is LoadCursorA
or LoadCursorW
.
Callbacks from Windows to Lisp
Once the message pump is up and going, the OS delivers the messages by
calling a Lisp function repeatedly. Lisp functions callable from the
foreign environment can be defined in the following manner:
; WndProc -- Window procedure for the window we will create
(fli:define-foreign-callable
(wndproc :result-type :long :calling-convention :stdcall)
((hwnd hwnd) (msg ulong)
(wparam ulong) (lparam ulong))
(case msg
(#.WM_PAINT (wndproc-paint hwnd msg wparam lparam))
#+console (#.WM_DESTROY (PostQuitMessage 0) 0)
(t (DefWindowProc hwnd msg wparam lparam))))
WM_PAINT
and
WM_DESTROY
at compile-time, allowing case
to
work. #+console means "include the next form only if :console is a
member of *features*".
PostQuitMessage
is called from the
IDE, the IDE shuts down. If PostQuitMessage
is not called
in console mode, the Win32 window does not close.
Starting the Program
Multiprocessing is always running under the Lispworks IDE but may or
may not be running using ILISP under Emacs. Using multiprocessing is
great because one can peek and poke at the program and its variables,
provide new or redefined functions which take effect immediately, and
even make Win32 API calls, all while the program is running and the
window is visible with all its buttons and menus active.
create-toplevel-window
ensures multiprocessing is running
when in console mode. The function
create-toplevel-window-run
performs the message pump
operation in console mode but not otherwise.
(defvar *reg-class-atom* (register-class))
create-toplevel-window-run
then only needs to call
CreateWindowEx
and optionally start the message pump.
The Lisp REPL and Win32 Development
When a Win32 application is running from within the Lispworks IDE,
one is able to enter Lisp forms at the IDE's REPL prompt. One can
view any variable, redefine any function, and make calls to
SendMessage
or any other Win32 function that doesn't
require context from the OS, such as being within a WM_PAINT. If one
redefines the function called when a button is clicked, the next click
of the button gets the new function. The Lispworks debug tools are
available. Other Lisp programs can be run simultaneously. Individual
functions within the running Win32 program can be called from the
REPL. Functions can be traced and untraced, advice can be added or
removed, and CLOS classes can be redefined on the fly with Lisp
guaranteeing that the slot additions or deletions happen in an orderly
fashion within the running program.
Making Direct Win32 Calls from CAPI
Lispworks includes CAPI, a cross-platform API for GUI program
development. CAPI is powerful and easy to use. For true
cross-platform capability, it is important to stay with 100%-pure CAPI.
(defclass image-pane (output-pane) ()
(:default-initargs
:display-callback 'draw-image))
...
(let ((pane-1 (make-instance 'image-pane))
...
(contain (make-instance 'column-layout
:description (list pane-1 other-pane))
:best-width 640
:best-height 480)))
...
(defun draw-image (pane x y width height)
(let ((hwnd (capi:simple-pane-handle pane)))
;; This returns the actual Win32 window handle
;; Now call CreateWindowEx with hwnd as the new window's parent
;; The Win32-defined window then covers the
;; CAPI window. After destroying or hiding the Win32 window, the
;; CAPI window is revealed.
;; Be careful not to create the window if it already is created.
....
SendMessage
is
to be called from the REPL, a valid hwnd must be available. A way to
have the hwnd available is to do a setf from within a function to
which the OS passes the hwnd. This may be used as a debug-only
technique or left as a permanent part of the program.
Interfacing to C
The OS makes many of the Win32 functions available always. Other
functions, for example the avicap32
video functions, exist in
DLLs which must be explicitly loaded. Third-party or custom-built
DLLs also require explicit loading.
and in Lisp
exports capCreateCaptureWindow=capCreateCaptureWindowW
exports capGetDriverDescription=capGetDriverDescriptionW
;; capCreateCaptureWindow
(fli:define-foreign-function
(capCreateCaptureWindow "capCreateCaptureWindowW")
((lpszWindowName :pointer) (dwStyle fli-dword) (x :int) (y :int)
(nWidth :int) (nHeight :int) (HWND fli-hwnd) (nID :int))
:result-type (:unsigned :long)
:module :avicap32
:documentation "Opens a video capture window.")
(fli:register-module "avicap32")
RAII and GC
A common C++ idiom is "Resource Acquisition Is Initialization", in
which a C++ object acquires an operating system resource, perhaps an
open file, in the constructor and releases the resource in the
object's destructor. These objects may have dynamic or indefinite
extent.
with-...
macro. The macro is responsible for acquiring the resource and
releasing it under an unwind-protect.
delete
or delete []
.
The destructor tears down the object, first releasing any acquired
resources via explicitly-programmed C++ code, then releasing the
object's memory via compiler-generated code as the destructor exits.
(defun
release-resources...
, and call the release function at the
point where the destructor would have been called in C++. After the
release function returns and there are no references to the object,
Lisp will free the object's memory during a future garbage collection.
COM
COM is widely used in Windows programming. Lispworks for Windows
includes a COM/Automation User Guide and Reference Manual and
the associated functions. I have not played with COM under Lisp and
only note the availability of the manuals and functions. Actually, I
did require com and automation and called the com:midl form, which
loaded a huge series of IDL files, amazing in breadth and extent. I
didn't actually make things happen with COM, though.
Beginning to Use the Power of Lisp
My first thought, when I finally completed my demo program, was "That
looks like any other Win32 program." There was nothing Lispy about
it. It was just Win32 code, programmable in any language. Different
languages are good for different problem sets. When I think of Perl, I
think of text. I think of numbers along with Fortran. When I think of
Lisp, I think of defining my own language. Macros are one of the
tools used to define embedded languages within Lisp and are part of
what makes Lisp the programmable programming language.
defvar
or defparameter
forms, other functions the macro creates
can use those vars and parameters, although closures can also be used
for that purpose. The use of defvars allows reference to those defvars
at the REPL in a multiprocessing environment while the Win32 program
has an open window and is executing. Whether defvars or closures are
used, absolutely zero store-and-retrieve infrastructure is needed.
with-foreign-strings
which takes a list of
pointer-name/string pairs and creates a nested series of
(fli:with-foreign-string...
calls, including creation of
the element-count and byte-count parameters with unique, predictible
names for each string. My setf-foreign-slot-values
, and
with-foreign-slot-values
macros also make for more
compact and readable code.
DefMsgHandler
macro that calls RegisterClass, defines the CLOS method for each
message to be handled, takes care of necessary housekeeping in
ubiquitous functions such as WM_PAINT
, and allows easy
definition of function bodies to be executed for each desired type of
message. Other macros are useful for defining pushbuttons, edit boxes,
list views, and other Win32 controls.
Conclusion
The example code presented in the text and in appendicies A-C places
an emphasis on staying in Lisp and accessing the Win32 API from
there. Paul Tarvydas has code in Appendix D
which demonstrates cooperation and interaction between C and Lisp. In
Paul's well-documented example, a C dll is used to drive the message
loop. The Lisp callback function's address is placed from within Lisp
into a variable in the dll.
Appendix A: "Hello, Lisp" Program #1
Here is a Win32 Lisp program that opens a GUI window displaying
"Hello, Lisp!". A detailed discussion of program specifics follows
the listing. The program listing contains necessary lines from the
header files which are #included
in a C/C++ program.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Header-file kinds of code
;
; Determine character type
(defun external-format ()
(if (string= (software-type) "Windows NT")
:unicode
:ascii))
(defparameter *class-name* "LWTestClass")
;--- Win32 SDK Constants
; Not all of these are verified by testing in code
; More are included than are used to allow play
(defconstant COLOR_WINDOW 5)
(defconstant CS_HREDRAW 1)
(defconstant CS_VREDRAW 2)
(defconstant CW_USEDEFAULT #x80000000)
(defconstant IDC_ARROW 32512)
(defconstant SW_SHOW 5)
(defconstant WM_CLOSE #x00000010)
(defconstant WM_DESTROY #x00000002)
(defconstant WM_PAINT #x0000000f)
(defconstant WS_BORDER #x00080000)
(defconstant WS_CAPTION #x00C00000)
(defconstant WS_CHILD #x40000000)
(defconstant WS_DISABLED #x08000000)
(defconstant WS_EX_CONTROLPARENT #x00010000)
(defconstant WS_EX_APPWINDOW #x00040000)
(defconstant WS_MAXIMIZEBOX #x00010000)
(defconstant WS_MINIMIZEBOX #x00020000)
(defconstant WS_OVERLAPPED #x00000000)
(defconstant WS_POPUP #x80000000)
(defconstant WS_SYSMENU #x00080000)
(defconstant WS_THICKFRAME #x00040000)
(defconstant WS_VISIBLE #x10000000)
; Aggregates
(defconstant WS_POPUPWINDOW (logior ws_popup ws_border ws_sysmenu))
(defconstant WS_OVERLAPPEDWINDOW (logior ws_overlapped ws_caption
ws_sysmenu ws_thickframe
ws_minimizebox ws_maximizebox))
;--- Win32 SDK data types
(fli:define-c-typedef BOOL (:unsigned :long))
(fli:define-c-typedef DWORD (:unsigned :long))
(fli:define-c-typedef HANDLE (:unsigned :long))
(fli:define-c-typedef HDC (:unsigned :long))
(fli:define-c-typedef HINSTANCE (:unsigned :long))
(fli:define-c-typedef HMENU (:unsigned :long))
(fli:define-c-typedef HWND (:unsigned :long))
(fli:define-c-typedef INT :int)
(fli:define-c-typedef LONG :long)
(fli:define-c-typedef LPCTSTR :pointer)
(fli:define-c-typedef LPSTR :pointer)
(fli:define-c-typedef LPVOID :long)
(fli:define-c-typedef LPFN :long) ;; Doesn't work as :pointer
(fli:define-c-typedef UINT (:unsigned :int))
(fli:define-c-typedef ULONG (:unsigned :long))
(fli:define-c-typedef wBYTE (:unsigned :char))
(fli:define-c-typedef PTR (:unsigned :long))
;--- Win32 SDK structures
; POINT
(fli:define-c-struct sPOINT
(x int)
(y int))
(fli:define-c-typedef POINT sPOINT)
; MSG
(fli:define-c-struct sMSG
(hwnd hwnd)
(wParam ulong)
(lParam ulong)
(time dword)
(point point))
(fli:define-c-typedef MSG sMSG)
; PAINTSTRUCT
(fli:define-c-struct sPAINTSTRUCT
(HDC hdc)
(fErase bool)
(rcPaint-x uint)
(rcPaint-y uint)
(rcPaint-width uint)
(rcPaint-height uint)
(fRestore bool)
(fIncUpdate bool)
(rgbReserved (:c-array wBYTE 32)))
(fli:define-c-typedef PAINTSTRUCT sPAINTSTRUCT)
; WndClass
(fli:define-c-struct sWNDCLASS
(style uint)
(lpfnWndProc lpfn)
(cbClsExtra int)
(cbWndExtra int)
(hInstance handle)
(hIcon handle)
(hCursor handle)
(hBrBackground handle)
(lpszMenuName ulong) ;; ulong so it can be set to null without error
(lpszClassName lpctstr))
(fli:define-c-typedef WNDCLASS sWNDCLASS)
;--- Win32 SDK functions
; BeginPaint
(fli:define-foreign-function
(BeginPaint "BeginPaint" :calling-convention :stdcall)
((hwnd hwnd) (lpPaintStruct ptr))
:result-type hdc)
; CreateWindowEx
(fli:define-foreign-function
(CreateWindowEx "CreateWindowEx" :dbcs :calling-convention :stdcall)
((dwExStyle dword) (lpClassName lpctstr) (lpWindowName lpctstr)
(dwStyle dword) (x uint) (y uint) (nWidth uint) (nHeight uint)
(hwndParent hwnd) (hMenu hmenu) (hInstance hinstance) (lpParam lpvoid))
:result-type hwnd)
; DefWindowProc
(fli:define-foreign-function
(DefWindowProc "DefWindowProc" :dbcs :calling-convenction :stdcall)
((hwnd ulong) (msg ulong) (wparam ulong) (lparam ulong))
:result-type ulong)
; DispatchMessage
(fli:define-foreign-function
(DispatchMessage "DispatchMessage" :dbcs :calling-convention :stdcall)
((MsgPtr ptr)) ;; We're passing a pointer but Lisp doesn't know
:result-type ulong)
; EndPaint
(fli:define-foreign-function
(EndPaint "EndPaint" :calling-convention :stdcall)
((hwnd hwnd) (lpPaintStruct ptr))
:result-type bool)
; GetLastError
(fli:define-foreign-function
(GetLastError "GetLastError" :calling-convention :stdcall)
()
:result-type dword)
; GetMessage
(fli:define-foreign-function
(GetMessage "GetMessage" :dbcs :calling-convention :stdcall)
((lpMsg ptr) (hwnd ulong) (MsgFiltMin ulong) (MsgFiltMax ulong))
:result-type bool)
; GetModuleHandle - with no module name specified; pass this a zero i.e. NULL
(fli:define-foreign-function
(GetModuleHandle-current "GetModuleHandle"
:dbcs :calling-convenction :stdcall)
((lpModuleName :long))
:result-type handle)
; Lisp helper
(defmacro current-module-handle ()
`(GetModuleHandle-current 0))
; GetModuleHandle - pass this a foreign string naming the desired module
(fli:define-foreign-function
(GetModuleHandle "GetModuleHandle" :dbcs :calling-convenction :stdcall)
((lpModuleName lpctstr))
:result-type handle)
; LoadCursor
(fli:define-foreign-function
(LoadCursor "LoadCursor" :dbcs)
((hInstance handle) (param (:unsigned :long)))
:result-type handle)
; PostQuitMessage
(fli:define-foreign-function
(PostQuitMessage "PostQuitMessage" :calling-convention :stdcall)
((nExitCode :int))
:result-type :void)
; RegisterClass
(fli:define-foreign-function
(RegisterClass "RegisterClass" :dbcs :calling-convention :stdcall)
((WndClass ptr))
:result-type bool)
; TextOut
(fli:define-foreign-function
(TextOut "TextOut" :dbcs :calling-convention :stdcall)
((HDC hdc) (nXStart uint) (nYStart uint)
(lpString lpctstr) (cbString uint))
:result-type bool)
; TranslateMessage
(fli:define-foreign-function
(TranslateMessage "TranslateMessage" :calling-convention :stdcall)
((msg-ptr ulong))
:result-type bool)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Code specific to one program
;
;--- Win32 SDK Callbacks
; WndProc -- Window procedure for the window we will create
(fli:define-foreign-callable
(wndproc :result-type :long :calling-convention :stdcall)
((hwnd hwnd) (msg (:unsigned :long))
(wparam (:unsigned :long)) (lparam (:unsigned :long)))
(case msg
(#.WM_PAINT (wndproc-paint hwnd msg wparam lparam))
#+console (#.WM_DESTROY (PostQuitMessage 0) 0)
(t (DefWindowProc hwnd msg wparam lparam))))
;--- Functions called from WndProc
(defun wndproc-paint (hwnd msg wparam lparam)
(fli:with-dynamic-foreign-objects ()
(let* ((ps (fli:allocate-dynamic-foreign-object :type 'paintstruct))
(hdc (beginpaint hwnd (fli:pointer-address ps))))
; Paint here
(fli:with-foreign-string (text-ptr ec bc :external-format (external-format))
"Hello, Lisp!"
(textout hdc 0 0 text-ptr ec))
(endpaint hwnd (fli:pointer-address ps))
0)))
;--- Main Functions
(defun register-class ()
(fli:with-foreign-string (cn-p ec bc :external-format (external-format))
*class-name*
;; Below - use with-dynamic... for automatic freeing. Make some pointers.
(let (rslt
(wc-p (fli:allocate-foreign-object :type 'wndclass))
(wp-p (fli:make-pointer :symbol-name "wndproc")))
(unwind-protect
(progn
(setf (fli:foreign-slot-value wc-p 'style) (logior cs_hredraw cs_vredraw))
(setf (fli:foreign-slot-value wc-p 'lpfnWndProc)
(fli:pointer-address wp-p))
(setf (fli:foreign-slot-value wc-p 'cbClsExtra) 0)
(setf (fli:foreign-slot-value wc-p 'cbWndExtra) 0)
(setf (fli:foreign-slot-value wc-p 'hInstance) (current-module-handle))
(setf (fli:foreign-slot-value wc-p 'hIcon) 0)
(setf (fli:foreign-slot-value wc-p 'hCursor) (LoadCursor 0 IDC_ARROW))
(setf (fli:foreign-slot-value wc-p 'hbrBackground) (1+ color_window))
(setf (fli:foreign-slot-value wc-p 'lpszMenuName) 0)
(setf (fli:foreign-slot-value wc-p 'lpszClassName) cn-p)
(setq rslt (RegisterClass (fli:pointer-address wc-p))))
(fli:free-foreign-object wp-p)
(fli:free-foreign-object wc-p)
(if (/= rslt 0)
rslt
(error (format nil "~&RegisterClass failed in reg-class. Error: ~a"
(GetLastError))))))))
(defvar *reg-class-atom* (register-class))
; CreateWindow
(defun create-toplevel-window-run (window-name)
"See create-window."
(fli:with-foreign-string ;; class name pointer
(cn-p ec bc :external-format (external-format)) *class-name*
(fli:with-foreign-string ;; window name pointer
(wn-p ec bc :external-format (external-format)) window-name
(let ((hwnd (createwindowex 0 cn-p wn-p
(logior ws_visible ws_overlappedwindow)
cw_usedefault cw_usedefault cw_usedefault cw_usedefault
;0 0 640 480
0 0 (GetModuleHandle-current 0) 0)))
(when (zerop hwnd)
(error (format nil "CreateWindow failed. GetLastError is ~a"
(GetLastError))))
#-console hwnd ;; Should return wParam in thread, what about here?
#+console ;; Message pump the thread
(fli:with-dynamic-foreign-objects ()
(let ((ms (fli:allocate-dynamic-foreign-object :type 'msg)))
(do ((gm (GetMessage (fli:pointer-address ms) 0 0 0)
(GetMessage (fli:pointer-address ms) 0 0 0)))
((zerop gm) (fli:foreign-slot-value ms 'wParam))
(TranslateMessage (fli:pointer-address ms))
(DispatchMessage (fli:pointer-address ms)))))))))
; CloseWindow
(defun close-window (hwnd)
(sendmessage hwnd WM_CLOSE 0 0))
(defmacro create-toplevel-window (window-name)
"Creates an overlapped window with title WINDOW-NAME.
Returns the window handle."
#-console `(create-toplevel-window-run ,window-name)
#+console `(progn
(when (null (mp:list-all-processes))
(mp:initialize-multiprocessing))
(mp:process-run-function ,window-name nil
'create-toplevel-window-run ,window-name)))
Appendix B: "Hello, Lisp!" Program #2
(require "win-header")
(in-package :wh)
(export '(initialize-appendix-b))
(DefMsgHandler appendix-b-proc MsgMap ()
(WM_PAINT
(text-out "Hello, Lisp!" 0 0 :color (RGB 0 0 255))
0))
(defun initialize-appendix-b ()
(DefActiveTopWindow "Hello" 'appendix-b-proc
:style ws_overlappedwindow :width 200 :height 100 :title "Hello Program"))
Appendix C: Program #3
(require "win-header")
(in-package :wh)
(export '(initialize-appendix-c))
; Following helps group the radio buttons
(defparameter *radio-list* nil)
; Define a message handler routine named testproc, inheriting
; from the base MsgMap. Others could inherit from this one...
(DefMsgHandler appendix-c-proc MsgMap (:hbrbackground (1+ color_btnface))
(WM_CREATE
(DefGroupBox hwndGroupBox "Wish List" 25 30 300 70) ;; left top width height
(DefPushButton hwndB1 "More Money" 30 48 90 40
((mMessageBox "Quit buying Lisp books." "You requested more money.")))
(DefPushButton hwndB2 "Youth" 121 48 90 40
((mMessageBox "Sorry, not even Lisp can help you there." "You requested Youth.")))
(DefPushButton hwndB3 "Better Lisp Skills" 212 48 90 40
((mMessageBox "Keep practising." "You requested better Lisp skills.")))
(DefRadioButton hwndRB1 "Lisp is cool" 40 105 100 20
:hwndlist *radio-list*)
(DefRadioButton hwndRB2 "Lisp is boss" 200 105 100 20
:hwndlist *radio-list*)
(SetCheck *hwndRB1*)
(with-listview (DefListView hwndListView "" 25 150 300 200)
((col "Lisp Book Title" 90)
(col "ISBN" 66)
(col "Author" 150)))
(DefEdit hwndEdit "Learning Lisp is fun" 40 372 160 20 ())
(DefAutoCheckBox hwndCheckBox "I like it" 210 372 70 20
((if (IsChecked *hwndCheckBox*)
(mMessageBox "I'm glad you like it" "Atta boy!")
(mMessageBox "Don't you like it?" "Whatsamatta U."))))
0)
(WM_PAINT
(with-font "Arial" -20 font-normal 0 0 0 ;; points weight italic bold underline
(text-out "I Like Lisp" 70 5 :color (RGB 0 0 255)))
(with-font "Arial" -11 font-bold 0 0 0
(text-out "Try it, you might like it, too!" 170 10
:color (RGB 255 0 0)))
0))
(defun initialize-appendix-c ()
(DefActiveTopWindow "Test" 'appendix-c-proc
:style ws_overlappedwindow :width 360 :height 450
:title "First Lisp Screenshot"))
Appendix D: Paul Tarvydas's Example
Here's an example that creates a windows class (in C) and gets invoked and
handled from LWW. It is similar to the "Hello" example in Petzhold, except
that it hooks to the LWW mainloop instead of creating its own. Probably it
ain't as pretty as it might be, due to my rustiness with Win32 (and my lack
of patience with it :-).
To use:
1) create a DevStudio Win32 DLL project called "wintest"
2) put the wintest.c file into the project
3) copy the run.lisp file into the project directory (so that the Debug
directory is a subdirectory)
4) Build the C project.
5) Set Project>>Settings>>Debug>>Executable for debug session to point
to the
lispworksxxx.exe.
6) Run the project - this should bring up lispworks.
7) Open run.lisp, compile and load it.
8) In the listener, type "(run)".
9) You should then see a window with "hello" in the middle of it.
The example window class is built and initialized in C (called from the lisp
mainline). The windows callbacks to this window are handled in lisp (eg. the
WM_PAINT message) - windows has been given a pointer to a lisp function
(Lisp_WndProc) and has been told to use it for callbacks. The lisp code
makes direct Win32 calls that display the "hello" text. Lisp uses FLI
foreign functions and foreign variables to set this up. [If one were doing
this on a real project, a less contrived flow of control would be chosen, but
this one appears to exercise the FLI calls that you were asking about].
[I welcome comments from anyone, re. style, simplification, etc.]
Paul Tarvydas
tarvydas at spamoff-attcanada dotca
//
// Example of creating a windows class in
// a DLL, then invoking it from Lisp.
//
// Convention for this example - routines
// that are written in C and are called by
// lisp are prefixed by "C_" and routines
// written in lisp called by C are prefixed
// by "Lisp_". Note that, using LWW's FLI functions
// and declarations everything could be done in Lisp.
// This example shows how to bounce between Lisp and
// C in the event that one might wish to "reuse"
// existing C code.
//
// The routines C_CreateClass and C_CreateWindow are
// written in C and exported from the DLL to be called
// by Lisp. Note that routines exported from a DLL
// are expected to use the :stdcall calling convention.
//
// The routine Lisp_WndProc is written in Lisp and
// exported (via FLI) so that it may be called
// by Windows. Note that Windows expects Lisp_WndProc
// to use the :stdcall calling convention.
#include
Windows is a trademark of Microsoft Corporation. Lispworks is a
trademark of LispWorks Ltd.
$Header: /cvsroot/cl-cookbook/cl-cookbook/win32.html,v 1.9 2007/01/16 20:58:33 ozten Exp $