oli:sd-inq-obj-contents-sysid
. The following code uses content sysids as the keys
for a hash table. The unique-objects
function returns the filtered list, i.e. a list which contains
only one representative for any given number of shared instances.
(in-package :de.clausbrod.filterinstances) (use-package :oli) (defun unique-objects(objects) (let ((ht (make-hash-table :test 'equal))) (dolist (obj objects) (setf (gethash (oli:sd-inq-obj-contents-sysid obj) ht) obj)) (loop for obj being the hash-values of ht collect obj)))
sd-inq-vp-drawlist-objects
which returns a list of all
currently visible objects.
sd-inq-vp-drawlist-objects
"compresses" its result as follows:
sd-inq-vp-drawlist-objects
would return a list containing:
(in-package :de.clausbrod.expanddrawlist) (use-package :oli) (defun flatten-assembly-mapcan(node) (cons node (mapcan #'flatten-assembly-mapcan (sd-inq-obj-children node)))) (defun expand-objects(objects) (loop for obj in objects nconc (remove-if-not #'sd-inq-part-p (flatten-assembly-mapcan obj)))) (defun show-part(p) (display (sd-inq-obj-pathname p))) (let ((displayed-objects (sd-inq-vp-drawlist-objects (sd-inq-current-vp)))) (mapc #'show-part (expand-objects displayed-objects)))
(in-package :de.clausbrod.expanddrawlist) (use-package :oli) (defun show-part(p) (display (sd-inq-obj-pathname p))) (defun visit-parts(obj) (if (sd-inq-part-p obj) (show-part obj) (mapc #'visit-parts (sd-inq-obj-children obj)))) (let ((displayed-objects (sd-inq-vp-drawlist-objects (sd-inq-current-vp)))) (mapc #'visit-parts displayed-objects)))
(oli:sd-sys-exec "cmd.exe")zu versuchen. Das führt dann aber dazu, dass CoCreate Modeling scheinbar hängt und nichts Erkennbares passiert. cmd.exe ist ein Kommandozeilenprogramm. Deswegen ist es völlig normal, dass ("grafisch") nichts passiert, wenn man cmd.exe als externes Programm ohne Parameter startet, zum Beispiel per sd-sys-exec. Dann wartet cmd.exe nämlich einfach im Hintergrund auf weitere Eingaben und tut sonst nichts. Will man cmd.exe in einem eigenen Terminalfenster (landläufig "DOS-Fenster" oder "command shell" oder "command prompt") starten und interaktiv laufen lassen, kann man das so erreichen:
(oli:sd-sys-exec "start cmd.exe")(Zu den Kommandozeilenparametern und Besonderheiten des Helferleins
start
siehe http://ss64.com/nt/start.html.)
Bonusfrage: Wenn cmd.exe
ein Kommandozeilenprogramm ohne grafische Oberfläche ist, wieso öffnet sich denn ein Terminalfenster, wenn man cmd.exe
aus Windows Explorer heraus startet?
Antwort: Weil Explorer entsprechend vorkonfiguriert ist - intern wird in so einem Fall nicht einfach nur cmd.exe
ausgeführt, sondern das moralische Äquivalent zu start cmd.exe
.
Bonusfrage 2: Woher weiss Windows eigentlich, wo cmd.exe
liegt? Muss man da nicht einen Pfad wie C:\Windows\System32\cmd.exe
angeben?
Hintergrund: In der Forumsfrage wurde ein solcher hartkodierter Pfad verwendet.
Antwort: Das Verzeichnis, in dem cmd.exe
liegt, taucht im Inhalt der Umgebungsvariablen PATH
auf, die Windows beim Starten von Programmen konsultiert. Damit ist die explizite Angabe eines Pfades unnötig. Mehr noch, sie ist sogar kontraproduktiv und fehlerträchtig - denn nicht auf jedem Rechner liegt das Windows-Verzeichnis unter C:\Windows
.
Bonusfrage 3: Wozu ist das eigentlich gut, so eine interaktive Instanz von cmd.exe
aus einer CAD-Applikation heraus zu starten?
Kopfkratzende erste Antwort: Für sachdienliche Hinweise dankbar
Zweite Antwort nach Eintreffen der angeforderten sachdienlichen Hinweise: Ziel war es offenbar letztlich, ein kleines interaktives Kommandozeilenprogramm zu starten - der Start von cmd.exe
war nur erster Test dafür.
symbol-function
.
There are other profilers out there for Common Lisp, but it is not always straightforward to make them work in CoCreate Modeling which implements a subset of CLtL1 only. So who knows, maybe someone out there will actually find this useful! To profile a function:(in-package :clausbrod.de) (export '(profile-function unprofile-function list-profiling-results)) (let ((profile-hashtable (make-hash-table))) (defun profile-function(func) "Instrument function for profiling" ;; check if symbol-plist already contains profiler flag (unless (get func :profile-original-symbol-function) (let ((original-symbol-function (symbol-function func))) (when original-symbol-function (setf (get func :profile-original-symbol-function) original-symbol-function) ;; mark as profiled ;; install profiler code (setf (symbol-function func) (lambda(&rest r) (let ((start-time (f2::seconds-since-1970))) (unwind-protect (if r (apply original-symbol-function r) (funcall original-symbol-function)) (let ((execution-time (- (f2::seconds-since-1970) start-time)) (accum (gethash func profile-hashtable))) (if accum (setf (gethash func profile-hashtable) (+ accum execution-time)) (setf (gethash func profile-hashtable) execution-time)) (format *standard-output* "~%Execution time for ~S: ~,10F~%" func execution-time)))))) )))) (defun unprofile-function(func) "Remove profiling instrumentation for function" (let ((original-symbol-function (get func :profile-original-symbol-function))) (when (remprop func :profile-original-symbol-function) (setf (symbol-function func) original-symbol-function)))) (defun list-profiling-results() "List profiling results in order of decreasing accumulated execution times" (format *standard-output* "~%Accumulated execution times:~%") (let (table-as-list) (maphash (lambda(k v) (push (cons k v) table-as-list)) profile-hashtable) (dolist (pair (sort table-as-list #'> :key #'cdr)) (format *standard-output* "~S: ~,10F~%" (car pair) (cdr pair))))) ) (f2::win-open-console-window) (setf si::*enter-break-handler* t) (use-fast-links nil)
(clausbrod.de:profile-function 'my-function)Now execute
my-function
at your heart's content. Every time the function is called, the profiler measures its execution time.
When the test session is completed, accumulated execution times can be listed as follows:
(clausbrod.de:list-profiling-results)And here is how to profile all functions in a given Lisp package:
(do-external-symbols (s (find-package "FOO")) (when (function s) (clausbrod.de:profile-function s)))My implementation differs almost entirely from Alex' version, which allows me to call it my own, but of course I owe thanks to Alex for starting the discussion in the forum and posting his original inspirational code! The code is now available as a Github project, see https://github.com/clausb/lisp-profiler. There is even a simple GUI dialog on top of the low-level profiling code: The version of the code shown above uses a SolidDesigner-specific way of getting the current time in high precision. The improved version in the Github project should work in other Lisp dialects as well. Fingers crossed.
sd-set-current-working-directory
API.
But when you call this function during startup (i.e. from
code in sd_customize
, or in code loaded from there), you may find that other customization code or even CoCreate Modeling itself
changes the current directory after your code runs. This is because CoCreate Modeling remembers the directory which was current
before the user closed the last session. When you restart the application, it will try to "wake up" in precisely that working directory.
To override this behavior, here's a simple trick:
sd_customize
(or, preferably, in code loaded from there), register an event handler for the SD-INTERACTIVE-EVENT
.
(in-package :de.clausbrod) (use-package :oli) (defun interactive-event-handler(&rest r) (sd-set-current-working-directory (user-homedir-pathname)) (sd-unsubscribe-event *SD-INTERACTIVE-EVENT* 'interactive-event-handler)) (sd-subscribe-event *SD-INTERACTIVE-EVENT* 'interactive-event-handler)This particular event handler sets the current working directory to the user's home directory, but this is of course just an example for a reasonable default.
node
:
(defstruct node (name "" :type string) (children nil :type list))Das reicht, um einen einfachen Teilebaum abzubilden. Ein Knoten kann entweder ein einfaches Teil repräsentieren - in diesem Fall hat er nur einen Namen. Wenn es sich um eine Baugruppe handelt, hält der Knoten eine Liste von Kindknoten in
children
.
(defmethod print-object ((node node) stream) (format stream "~A [~A] " (node-name node) (if (node-children node) "asm" "part")))Damit man einen
node
halbwegs kompakt ausgeben kann, definieren wir uns
ein zur Struktur passendes generisches print-object
. Aus der etwas langatmigen Darstellung
einer Strukturinstanz wie
#S(NODE :NAME "42" :CHILDREN (#S(NODE :NAME "p42" :CHILDREN NIL)))wird dadurch
42 [asm]Testbaugruppen baut man sich einfach per Strukturliteral. Beispiel:
(let ((tree #S(NODE :NAME "a1" :CHILDREN (#S(NODE :NAME "p1") #S(NODE :NAME "p2") #S(NODE :NAME "a11" :CHILDREN (#S(NODE :NAME "p11") #S(NODE :NAME "p12"))) #S(NODE :NAME "a12" :CHILDREN (#S(NODE :NAME "p13") #S(NODE :NAME "p14")))))))Mit dieser Vorbereitung können wir nun endlich des Kollegen Codeschnippsel betrachten. Naja, eine leicht angepasste Variante davon jedenfalls:
(defun flatten-assembly-apply-nconc(node) (cons node (apply #'nconc (mapcar #'flatten-assembly-apply-nconc (node-children node)))))Ruft man
flatten-assembly-apply-nconc
für die obige Testbaugruppe (flatten-assembly-apply-nconc tree)
, erhält man
dank des von uns definierten print-object
in der REPL in etwa folgendes:
(a1 [asm] p1 [part] p2 [part] a11 [asm] p11 [part] p12 [part] a12 [asm] p13 [part] p14 [part])Es entsteht also in der Tat eine flache Liste - wie schön. Sich zu verbildlichen, warum die Funktion die gewünschten Effekt hat, braucht schon einen kleinen Moment - und vielleicht auch den einen oder anderen Blick ins Lisp-Manual, um sich der genauen Funktionsweise von nconc oder mapcar zu vergewissern. Entscheidend ist unter anderem, dass Lisp-Listen letztlich Ketten von cons-Zellen sind, deren letztes Element auf
nil
verweist, und dass node-children
genau solche nil-Werte passend liefert, die
von mapcar
und nconc
auch brav durchgeschleust werden.
flatten-assembly-apply-nconc
setzt das "destruktive" nconc ein, um weniger
Speicher allozieren zu müssen. Was mich gleich zu der Frage geführt hat, ob es vielleicht noch effizienter geht,
und so entstanden folgende Varianten:
(defun flatten-assembly-apply-append(node) (cons node (apply #'append (mapcar #'flatten-assembly-apply-append (node-children node))))) (defun flatten-assembly-mapcan(node) (cons node (mapcan #'flatten-assembly-mapcan (node-children node)))) ;; version using an accumulator (defun flatten-assembly-accumulator(node &optional acc) (cond ((null node) acc) ((listp node) (flatten-assembly-accumulator (first node) (flatten-assembly-accumulator (rest node) acc))) ((null (node-children node)) (cons node acc)) ;; assembly case, i.e. a node with children (t (cons node (flatten-assembly-accumulator (node-children node) acc)))))Diese Varianten habe ich hintereinander in drei Lisp-Implementierungen ausgemessen, und zwar in CLISP 2.49, Clozure CL 1.1 und SBCL 1.2.10. Weil SBCL sich zumindest auf Mac OS bei kurzläufigen Tests zickig anstellt und keine Messdaten liefert, habe ich die jeweilige Testfunktion in einer Schleife 100000mal aufgerufen:
(let ((tree #S(NODE :NAME "a1" :CHILDREN (#S(NODE :NAME "p1") #S(NODE :NAME "p2") #S(NODE :NAME "a11" :CHILDREN (#S(NODE :NAME "p11") #S(NODE :NAME "p12"))) #S(NODE :NAME "a12" :CHILDREN (#S(NODE :NAME "p13") #S(NODE :NAME "a121" :CHILDREN (#S(NODE :NAME "a1211" :CHILDREN (#S(NODE :NAME "p1211"))))) #S(NODE :NAME "p14"))))))) (defun run-test(function-symbol) (gc) (format t "~%Test function: ~A~%" (symbol-name function-symbol)) (print (time (dotimes (i 100000) (run-test-raw function-symbol))))) ) (run-test 'flatten-assembly-apply-append) (run-test 'flatten-assembly-apply-nconc) (run-test 'flatten-assembly-mapcan) (run-test 'flatten-assembly-accumulator)
Variante | Lisp-Implementierung | Laufzeit (µs) | Allokation (Bytes) |
---|---|---|---|
flatten-assembly-apply-append | CLISP | 3173017 | 72000000 |
flatten-assembly-apply-nconc | CLISP | 3034901 | 56000000 |
flatten-assembly-mapcan | CLISP | 2639819 | 38400000 |
flatten-assembly-accumulator | CLISP | 4959644 | 46400000 |
flatten-assembly-apply-append | CCL | 70407 | 52800000 |
flatten-assembly-apply-nconc | CCL | 54713 | 36800000 |
flatten-assembly-mapcan | CCL | 128232 | 19200000 |
flatten-assembly-accumulator | CCL | 20997 | 19200000 |
flatten-assembly-apply-append | SBCL | 37000 | 52768224 |
flatten-assembly-apply-nconc | SBCL | 25000 | 36798464 |
flatten-assembly-mapcan | SBCL | 29000 | 19169280 |
flatten-assembly-accumulator | SBCL | 22000 | 19169280 |
flatten-assembly-apply-nconc
und flatten-assembly-mapcan
am besten ab. Dies ist aber mit Vorbehalt
gesagt, denn in HCL musste ich den Code - mangels Compiler-Lizenz - interpretiert ablaufen lassen,
was das Performancebild vermutlich stark verfälscht.
LINE
and hitting the ENTER
key. The command would prompt for more input and provide hints in the
UI on what to do next, such as selecting the kind of line to be drawn, or picking points
in the 2D viewport (the drawing canvas). The example above also illustrates that commands
such as LINE RECTANGLE
could loop, i.e. you could create an arbitrary amount of rectangles;
hence the need to explicitly END
the command.
Essentially, each of the commands in ME10 was a domain-specific mini-language,
interpreted by a simple state machine.
The original architects of SolidDesigner (now known as CoCreate Modeling)
chose Lisp as the new extension and customization language, but they also wanted
to help users with migration to the new product. Note, however, how decidedly un-Lispy ME10's
macro language actually was:
extrude
).
extrude
commands in parentheses.
define-symbol-macro
yet. And thus,
CoCreate Modeling's Lisp evaluator extensions were born.
To be continued...
extrude
is somehow... different from all the other kids. All we need is a simple experiment.
First, enter extrude
in
CoCreate Modeling's user input line: The Extrude dialog
unfolds in all its glory, and patiently awaits your input.
Now try the same with print
: All you get is an uncooperative
"Lisp error: The variable PRINT is unbound". How disappointing.
But then, the behavior for print
is expected, considering the usual
evaluation rules for Common Lisp,
particularly for symbols. As a quick reminder:
extrude
& friends belong to the symbol jet-set in CoCreate Modeling. For them,
the usual evaluation rules for functions don't apply (pun intended).
Using symbol properties
as markers, they carry a backstage pass and can party anywhere.
For members of the extrude
posse, it doesn't really matter if you use them as an
atom, in the first position of a list, or anywhere else: In all cases, the function which
they refer to will be executed right away - by virtue of an extension to the evaluator
which is unique to CoCreate Modeling's implementation of Common Lisp.
You can create such upper-class symbols yourself - using a macro called defaction
.
This macro is also unique to CoCreate Modeling. Functions
defined by defaction
are called, you guessed it, action routines.
But why, you ask, would I want such a feature, particularly if I know that it breaks with
established conventions for Lisp evaluation?
Well, precisely because this feature breaks with the established rules.
To be continued...
print
and
CoCreate Modeling commands such as extrude
differ and how they
interact, you've come to the right place.
Usually, I call a dialog like this: (set_pers_context "Toolbox-Context" function) Or like this: function As soon as I add parentheses, however, the "ok action" will be called: (function)When highway45 talks of "functions" here, he actually means commands like
extrude
or turn
. So, (set_pers_context "Toolbox-Context" extrude)? Really? Wow!
set_pers_context
is an internal CoCreate Modeling function dealing with
how UI elements for a given command are displayed and where. I was floored -
first, by the fact that an end user found a need to call an internal function like this,
and second, because that magic incantation indeed works "as advertised" by highway45.
For example, try entering the following in CoCreate Modeling's user input line:
(set_pers_context "Toolbox-Context" extrude)Lo and behold, this will indeed open the
Extrude
dialog, and CoCreate Modeling
now prompts for more input, such as extrusion distances or angles.
What's so surprising about this, you ask? If you've used CoCreate Modeling for a while,
then you'll know that, as a rule of thumb, code enclosed in parentheses won't prompt
for more input, but will instead expect additional parameters in the command line itself.
For example, if you run (extrude)
(with parentheses!) from the user input line, Lisp will
complain that the parameter "DISTANCE is not specified". But in highway45's example, there
clearly was a closing parenthesis after extrude
, and yet the Extrude command started to
prompt!
So is set_pers_context
some kind of magic potion? Try this:
(print extrude)The Extrude dialog opens and prompts for input! Seems like even
print
has
magic powers, even though it's a plain ol' Common Lisp standard function!
Well, maybe there is something special about all built-in functions? Let's test this out and
try a trivial function of our own:
(defun foobar() 42) (foobar extrude)Once more, the dialog opens and awaits user input! So maybe it is neither of
set_pers_context
, print
or foobar
that is magic - but instead extrude
.
We'll tumble down that rabbit hole next time.
To be continued...
(defun test() (test_dialog)) (in-package :clausbrod.de) (use-package :oli) (sd-defdialog 'test_dialog :ok-action '(display "test_dialog"))In part 3 of this mini-series, we figured out that the #: prefix indicates an uninterned symbol - and now we can solve the puzzle! Earlier, I had indicated that
sd-defdialog
automatically exports dialog
names into the default package. To perform this trick, somewhere in the bowels of
the sd-defdialog
macro, the following code is generated and executed:
(shadowing-import ',name :cl-user) ;; import dialog name into cl-user package (export ',name) ;; export dialog name in current package (import ',name :oli) ;; import dialog name into oli package (export ',name :oli) ;; export dialog name from the oli packageAs a consequence, the dialog's name is now visible in three packages:
cl-user
)
oli
)
clausbrod.de
)
shadowing-import inserts each of symbols into package as an internal symbol, regardless of whether another symbol of the same name is shadowed by this action. If a different symbol of the same name is already present in package, that symbol is first uninterned from package.That's our answer! With this newly-acquired knowledge, let's go through our code example one more and final time:
(defun test() (test_dialog))Upon loading this code, the Lisp reader will intern a symbol called
test_dialog
into the current (default) package. As test_dialog
has not
been defined yet, the symbol test_dialog
does not have a value; it's just
a placeholder for things to come.
(in-package :clausbrod.de) (use-package :oli)We're no longer in the default package, and can freely use
oli:sd-defdialog
without
a package prefix.
(sd-defdialog 'test_dialog :ok-action '(display "test_dialog"))
sd-defdialog
performs (shadowing-import 'test_dialog :cl-user),
thereby shadowing (hiding) and uninterning the previously interned test_dialog
symbol.
Until we re-evaluate the definition for (test)
, it will still refer to the
old definition of the symbol test_dialog
, which - by now - is a) still without
a value and b) uninterned, i.e. homeless.
Lessons learned:
(test)
function would have saved
us all that hassle.
Phew.
(defun test() (test_dialog)) (in-package :clausbrod.de) (use-package :oli) (sd-defdialog 'test_dialog :ok-action '(display "test_dialog"))Load the above code, run
(test)
, and you'll get:
sd-defdialog
macro automatically exports the name of the new
dialog (in this case, test_dialog
) into the default package. Hence, you'd expect that
the function (test)
, which is in the default package, would be able to call that dialog!
Astute readers (and CoCreate Modeling's Lisp compiler) will rightfully scold me for using
(in-package)
in the midst of a file. However, the error doesn't go away if you split up
the above code example into two files, the second of which then properly
starts with (in-package)
. And in fact, the problem originally manifested itself in a
multiple-file scenario. But to make it even easier for readers to run the test themselves,
I just folded the two files into one.
Lisp actually provides us with a subtle hint which I ignored so far: Did you notice
that the complaint is about a symbol #:TEST_DIALOG
, and not simply TEST_DIALOG
?
The #:
prefix adds an important piece to the puzzle. Apparently, Lisp thinks
that TEST_DIALOG
is not a normal symbol,
but a so-called uninterned symbol. Uninterned symbols are symbols which don't
belong to any Lisp package - they are homeless. For details:
TEST_DIALOG
turned into an uninterned symbol. We would have expected it to
be a symbol interned in the clausbrod.de
package, which is where the dialog is defined!
Those who are still with me in this series will probably know where this is heading.
Anyway - next time, we'll finally
solve the puzzle!
(defun test() (test_dialog)) (in-package :clausbrod.de) (use-package :oli) (sd-defdialog 'test_dialog :ok-action '(display "test_dialog"))Here is what happens if you save this code into a file, then load the file into CoCreate Modeling and call the
(test)
function:
test
is defined in the default Lisp package.
After its definition, we switch into a different package (clausbrod.de
), in
which we then define a CoCreate Modeling dialog called test_dialog
.
The (test)
function attempts to call that dialog. If you've had any exposure with
other implementations of Lisp before, I'm sure you will say: "Well, of course the system
will complain that TEST_DIALOG
is undefined! After all, you define it in package
clausbrod.de
, but call it from the default package (where test
is defined).
This is trivial! Go read
The Complete Idiot's Guide to Common Lisp Packages
instead of wasting our time!"
To which I'd reply that sd-defdialog
, for practical reasons I may go into in a future blog
post, actually makes dialogs visible in CoCreate Modeling's default package. And since
the function test
is defined in the default package, it should therefore have
access to a symbol called test_dialog
, and there shouldn't be any error messages, right?
To be continued...
(defun test() (test_dialog)) (in-package :clausbrod.de) (use-package :oli) (sd-defdialog 'test_dialog :ok-action '(display "test_dialog"))Copy/paste this code into a file called
test.lsp
, then load the file
into a fresh instance of CoCreate Modeling. Run the test
function by entering (test)
in
the user input line. Can you guess what happens now? Can you explain it?
To be continued...
;; (C) 2009 Claus Brod ;; ;; Demonstrates how to convert models into STEP format ;; in batch mode. Assumes that STEP module has been activated. (in-package :clausbrod.de) (use-package :oli) (export 'pkg-to-step) (defun convert-one-file(from to) (delete_3d :all_at_top) (load_package from) (step_export :select :all_at_top :filename to :overwrite) (undo)) (defun pkg-to-step(dir) "Exports all package files in a directory into STEP format" (dolist (file (directory (format nil "~A/*.pkg" dir))) (let ((filename (namestring file))) (convert-one-file filename (format nil "~A.stp" filename)))))To use this code:
(clausbrod.de:pkg-to-step "c:/allmypackagefiles")
*.pkg
) file in the specified directory, a STEP file will be generated in the
same directory. The name of the STEP file is the original filename with .stp
appended to it.
In pkg-to-step
, the code iterates over the list of filenames returned from
(directory)
. For each package file, convert-one-file
is called, which performs
the actual conversion:
Step | Command |
---|---|
Delete all objects in memory (so that they don't interfere with the rest of the process) | delete_3d |
Load the package file | load_package |
Save the model in memory out to a STEP file | step_export | Revert to the state of affairs as before loading the package file | undo |
delete_3d
, load_package
,
step_export
and undo
. (These are the kind of commands which are captured in a recorder
file when you run CoCreate Modeling's recorder utility.) Around those commands, we use
some trivial Common Lisp glue code - essentially, dolist over
the results of directory. And that's all, folks
Astute readers will wonder why I use undo
after the load operation rather than delete_3d
the model. undo
is in fact more efficient in this kind of scenario, which is
an interesting story in and of itself - and shall be told some other day.
Official name | Colloquial | Versions | When |
---|---|---|---|
CoCreate OneSpace Designer Dynamic Modeling | ? | 11 | 2001-2002 |
PTC CoCreate Modeling | CoCreate Modeling | 16 and later | 2008- |
CoCreate OneSpace Modeling | OneSpace Modeling | 15 | 2007 |
CoCreate OneSpace Designer Modeling | OSDM | 11.6-14 | 2002-2006 |
HP PE/SolidDesigner | SolidDesigner | 1-7 | 1992-1999 |
CoCreate SolidDesigner | SolidDesigner | 8-9 | 2000-2001 |
SolidDesigner.exe
Anyway - I'll have to admit that I start to like "CoCreate Modeling" as well. It's reasonably
short, simple to remember, alludes to what the product does, and it reminds users of our
past as CoCreate - which is a nice nostalgic touch for old f*rts like me who've
been with the team for almost two decades now...
SDPIXELFORMAT
, and give
it a value of SOFTWARE
. To set the environment variable, use the System Control Panel.
Click sequence in Windows XP:
System
control panel
Advanced
tab
System and Maintenance
, then System
Advanced System Settings
; this may pop up a user-access control dialog which you need to confirm
Environment Variables
SDPIXELFORMAT
and set the value to SOFTWARE
.
;; -*-Lisp-*- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Description: Wrapper to run Frank Buss' functional geometry code ;; in CoCreate Modeling ;; Author: Claus Brod ;; Language: Lisp ;; ;; (C) Copyright 2008 Claus Brod, all rights reserved ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; (in-package :clausbrod.de) (use-package :oli) (export '(plot-escher)) ;; Allow using lambda without quoting it via #' first ;; (No longer required in CoCreate Modeling 2008 and later.) (defmacro lambda (&rest body) `(function (lambda ,@body))) (defparameter *our-loadpath* *load-truename*) (load (format nil "~A/functional.lsp" (directory-namestring *our-loadpath*))) ;; Modeling-specific plotter function (defun plot-annotation (p) (let ((tempfile (format nil "~A/test.mac" (oli:sd-inq-temp-dir))) (scale 500.0)) (startup::activate-annotation) (with-open-file (s tempfile :direction :output :if-exists :supersede) (format s "line~%") (dolist (line (funcall p '(0 0) '(1 0) '(0 1))) (destructuring-bind ((x0 y0) (x1 y1)) line (format s " ~D,~D ~D,~D~%" (* scale (float x0)) (* scale (float y0)) (* scale (float x1)) (* scale (float y1))))) (format s "end")) (oli:sd-execute-annotator-command :cmd (format nil "input '~A'" tempfile)) (docu::docu_vp :fit_vp) (delete-file tempfile))) ;; Shortcut for the Escher fish drawing (defun plot-escher() (plot-annotation *fishes*))The loader code adds the definition for the lambda macro which is missing so far in CoCreate Modeling, loads Frank's code, and then adds a plotter function which creates output in a 2D Annotation window. Usage instructions:
functional.lsp
.
(clausbrod.de:plot-escher)
Common Lisp is also used as a user accessible extension language for HP PE/SolidDesigner. It is a standardized, open programming language, not a proprietary one as in HP PE/ME10 and PE/ME30, and the developers of HP PE/SolidDesigner believe that this will prove to be an immense advantage.SolidDesigner was the original product name; ME10 and ME30 were predecessor products which implemented their own little macro interpreters. Back then, we were a bit cautious about the potential benefits we'd reap, as the product was still in its early days. By now, however, we can say that Common Lisp was a key factor in helping a fairly small team of developers keep pace with the big guns in the industry, due to all the well-known productivity features in Lisp, such as macros, the REPL, or automatic memory management. The HP Journal article describes how we use macros to define a domain-specific language called action routines, which are state machines which guide users through commands. Later, we extended that concept by automatically generating UI for those commands: Using the sd-defdialog macro, application developers can implement full-blown commands in just a few lines of code, without having to write any (or at least hardly any) code for services such as:
I've been working with the fine folks of CoCreate for so long now that they tattoed an asset number on my forehead. In fact, I started at HP, in the very department (MDD, which stood for Mechanical Design Division) from which our CAD products were born, and later left HP, together with the rest of us CAD-heads .-), to form what is now known as CoCreate.
As a member of the OneSpace Modeling development team, I serve as the software architect for our OneSpace Modeling product line.
I specialize in all development areas related to general software engineering, as well as systems programming. That's why I deal a lot with things like operating system dependencies, memory management, file handling, globalization or 3D graphics. What I really like about my job at CoCreate is that I can still occasionally fiddle with the low-level, close-to-the-bare-metal stuff, and yet have a chance to work on higher levels of our software, too. Since OneSpace Modeling is a really big and impressive piece of software, I also got to learn an awful lot about managing large projects and about software development techniques. As a recent example, we have been introducing XP techniques into the lab over the past two or three years. Which is quite a ride, given that XP originated from much smaller projects. It's cool that even though we are an established company with a track record of close to 20 years or so, we're still flexible enough to integrate new approaches and learn from them.
My parents probably still don't have the faintest idea what my job is all about .-) But then, it is tricky to explain what it's like to work here without going into lots of boring details. If you really want to know, you could check out some of the articles which we wrote about SolidDesigner (as our product was called back then) in 1995 for HP Journal. In those days, I was working on the object management layer in our code: "Providing CAD Object Management Services through a Base Class Library" summarizes its purpose (local copy is here). If you think this is thrilling stuff, why don't you send us your résumé? .-) And if show me yours, I'll show you mine (access password-protected, contact me for details).
Sometimes, when I find a funky software development problem either at CoCreate or in my own projects, I blog about it in my software development blog, so if you like techno-babble, go check it out!
I also keep some job-related information in my XING and LinkedIn accounts.
(line :two_points 100,100 0,0)Common Lisp connoisseurs will notice that this is decidedly non-standard behavior. Those commas aren't supposed to be there; instead, commas serve their purpose in list quoting, particularly in macro definitions. (For a refresher, check out The Common Lisp Cookbook - Macros and Backquote.) And in any other implementation of Lisp, this code would indeed result in an error message such as "comma is illegal outside of backquote". OneSpace Modeling's embedded Lisp, however, will notice a pair of literal numbers and assume that what the user really meant to specify is a structure of type
gpnt2d
, which holds x
and y
slots for the coordinates. And so
what is really evaluated is more like this:
(line :two_points (oli:make-gpnt2d :x 100 :y 100) (oli:make-gpnt2d :x 0 :y 0))
oli
is the Lisp package which exports the gpnt2d
structure as well as its accessor
and constructor functions.
This explicit syntax is actually required whenever you need to specify coordinates
using non-literals, such as when the actual coordinates are the results of
mathematical calculations. For instance, vector syntax is not recognized
in the following:
(line :two_points (+ 50 50),100 0,0)Now you'll get the expected error message reporting that "a comma has appeared out of a backquote". To make this work, you'd have to say:
(line :two_points (oli:make-gpnt2d :x (+ 50 50) :y 100) 0,0)But despite this limitation, the vector syntax extension was tremendously important for us: Coordinates can be entered in all kinds of places in the user interface where casual users would never suspect that what they are entering is actually thrown into funny engines which the propellerheads at CoCreate call "the Lisp reader" and "the Lisp evaluator".
(setf (get 'some-symbol some-indicator) some-value)And to inquire a symbol property, you just say something like
(get 'some-symbol some-indicator)
.
some-indicator
can basically be any type, and so I wasn't sure what my
co-worker meant when he said that he couldn't get strings to work, until
he explained the details to me: He was calling some Lisp-based API
function in our product, and that function returns a property list.
Unfortunately, that property list was special in that somebody had
stuffed a string into it as an indicator, and so the property list
looked somehow like this:
("foo" 42 "bar" 4711)And indeed, if you now try to inquire the "foo" property using
(get 'some-symbol "foo")
, all you get is - nil
.
To retrieve a property value, get
walks the list and compares each
indicator in the list with "foo" (in this example) - using eq
.
From which we can immediately conclude:
eq
checks for object equality, not just value equality. Which means
that things like literal (!) strings or characters cannot be indicators!
(get 'some-symbol "foo")
, and that "foo" string literal
creates a new string object. While that new object happens to have
the same value as the "foo" string in the property list, it is not the same object.
Indeed, the
Common Lisp HyperSpec
is quite clear on that topic:
"Numbers and characters are not recommended for use as indicators in
portable code since get tests with
eq rather than eql, and
consequently the effect of using such indicators is implementation-dependent."
It all boils down to the simple fact that (eq "foo" "foo")
returns nil
.
Now hopefully we can fix the API which returned those inadequate property
lists to my co-worker's code, but his code also needs to run in older
and current installations, and so he needed a workaround of some sort.
His first idea was to get the property list and fix it up in a preprocessing
step before using get
or getf
for lookup, i.e. something like this:
(defun fix-plist(plist old-indicator new-indicator) (let ((cnt 0)) (mapcar #'(lambda(item) (incf cnt) (if (and (oddp cnt) (equal item old-indicator)) new-indicator item)) plist))) (setf my-symbol 42) (setf (get 'my-symbol "indicator") "value") ;; mess up plist (print (get 'my-symbol "indicator")) ;; returns NIL (print (getf (fix-plist (symbol-plist 'my-symbol) "indicator" :indicator) :indicator))This works, kind of - but it is actually quite ugly. Sure, with this code, we should be able to safely move ahead, especially since I also closed that office window in the meantime, but still: I really hope I'm missing something here. Any other ideas out there?
sd-defdialog
which is provided by the "Integration Kit" library which
ships with OneSpace Modeling. This API is, in fact, implemented
using defmacro
, i.e. sd-defdialog
is itself a Lisp macro. So if a user
writes code which builds on sd-defdialog
and then calls the result a macro,
he's actually not that far from the truth - although, of course, still
incorrect.
Anwender brauchen schnelle Lösungen, Entwickler gründliche Wenn es im Produkt klemmt, will der Anwender möglichst fix eine Lösung, um weiterarbeiten zu können - selbst wenn die Lösung so hemdsärmlig und kurzlebig wäre, daß sie einem Softwareentwickler Magengrimmen verursacht. Der Entwickler hingegen hat ein Interesse daran, Schwierigkeiten und Tathergang möglichst vollständig aufzuklären: Was ist der Kern des Problems, und welche Beobachtungen haben damit nichts zu tun? War es vielleicht doch ein Anwenderfehler und wie könnte man den in Zukunft vermeiden? Oder ist es ein Fehler in der Software, und wie kann ich den ohne Nebenwirkungen korrigieren, so daß ich mich später nie mehr darum kümmern muß? Also fragt der Entwickler vier- oder fünfmal nach den genaueren Umständen, um die Lage zu sondieren und falsche Vermutungen auszuschließen. Naja, jedenfalls tue ich das gerne. Vielleicht habe mir auf diese Weise so nach und nach das "Totreder"-Image eingehandelt. Kollege Kunde? Wohl doch eher König! Auch wenn die Umgangsformen im Forum kollegial und locker sind, und auch wenn ich tausendmal betone, daß ich das Forum als Privatmann besuche: Aus Kundensicht stehe ich im Zweifel auf der anderen Seite und hafte für all die kleinen oder großen Probleme mit, die der Anwender mit CoCreate-Produkten oder mit CoCreate selbst hatte oder hat. Im Forum geäußerter Werkstolz oder auch der Versuch, falschen Behauptungen entgegenzutreten, wird deswegen besonders kritisch beurteilt.Simple Wahrheiten, denke ich heute - und daß die Vorstellung, mit Kunden feierabends am virtuellen Stammtisch klönen zu können, doch eher naïv war. Und wenn ich noch so darauf beharre, als Privatmann an den Diskussionen teilzunehmen: Das Verhätnis ist und bleibt nun einmal asymmetrisch. Wäre die Betreuung von Foren offizieller Bestandteil meines Jobs, so müßte ich als beauftragter Vertreter meiner Firma mit Angriffen und Auseinandersetzungen leben - und könnte das dann auch gut, denn ich wäre ja nicht persönlich gemeint, oder zumindest könnte ich mir das plausibel einreden. Ich war indes privat und aus Spaß an der Freud' dabei. Am Ende war vom Spaß wenig übrig, also hieß es für mich: Loslassen üben! Die deutschen Foren funktionieren schließlich auch ohne mich prima. Sehr wahrscheinlich besser als zuvor. Bin ich eine Mimose? Gut möglich; ich weiß es nicht. Nur daß mir Auseinandersetzungen im Forum zuweilen die ganze Woche verdorben haben, das weiß ich. Und daß ich das nicht mehr erleben möchte. Was bedeutet das nun für andere Foren? Dort läuft es besser. Vielleicht liegt es daran, daß dort in Englisch diskutiert wird und der Ton schon deswegen ein anderer ist. Jedenfalls werde ich einstweilen Foren wie das internationale CoCreate-Anwenderforum weiter besuchen. Und dieser Blog? Und die FAQ-Seiten, die CoCreate-Produkte betreffen? Nun, auf dieser Website trifft sich offenbar ein anderes Publikum: CAD-Administratoren, Angehörige von Partnerfirmen, Programmierer. Diskussionen, die sich hier ergeben, haben in der Tat eher kollegialen Charakter. Ich mache hier also weiter.
C:\Program Files
,
C:\Documents and Settings
). And when using software, you might be running
cmd.exe
without even knowing it. Many applications can run external helper
programs upon user request, be it through the UI or through the application's
macro language.
The test environment is a directory c:\temp\foo bar
which contains
write.exe
(copied from the Windows system directory) and two text files, one of
them with a blank in its filename.
Now we open a DOS shell:
Note that we had to quote the pathname to make theC:\>dir c:\temp\foo bar Volume in drive C is IBM_PRELOAD Volume Serial Number is C081-0CE2 Directory of c:\temp File Not Found Directory of C:\ File Not Found C:\>dir "c:\temp\foo bar" Volume in drive C is IBM_PRELOAD Volume Serial Number is C081-0CE2 Directory of c:\temp\foo bar 03/18/2006 03:08 PM <DIR> . 03/18/2006 03:08 PM <DIR> .. 01/24/2006 11:19 PM 1,516 foo bar.txt 01/24/2006 11:19 PM 1,516 foo.txt 03/17/2006 09:44 AM 5,632 write.exe 3 File(s) 8,664 bytes 2 Dir(s) 17,448,394,752 bytes free
DIR
command work.
Nothing unusual here; quoting is a fact of life for anyone out there
who ever used a DOS or UNIX shell.
Trying to start write.exe
by entering c:\temp\foo bar\write.exe
in the
DOS shell fails; again, we need to quote:
And if we want to loadC:\>"c:\temp\foo bar\write.exe"
foo bar.txt
into the editor, we need to quote
the filename as well:
Still no surprises here. But let's suppose we want to run an arbitrary command from our application rather than from the command prompt. The C runtime library provides the system() function for this purpose. It is well-known that under the hoodC:\>"c:\temp\foo bar\write.exe" "c:\temp\foo bar\foo bar.txt"
system
actually runs cmd.exe
to do its job.
When running this code, it reports that#include <stdio.h> #include <process.h> int main(void) { char *exe = "c:\\temp\\foo bar\\write.exe"; char *path = "c:\\temp\\foo bar\\foo bar.txt"; char cmdbuf[1024]; _snprintf(cmdbuf, sizeof(cmdbuf), "\"%s\" \"%s\"", exe, path); int ret = system(cmdbuf); printf("system(\"%s\") returns %d\n", cmdbuf, ret); return 0; }
system()
returned 0, and write.exe
never starts, even though we quoted both the name of the executable and
the text file name.
What's going on here? system()
internally runs cmd.exe
like this:
Try entering the above in the command prompt: No editor to be seen anywhere! So when we runcmd.exe /c "c:\temp\foo bar\write.exe" "c:\temp\foo bar\foo bar.txt"
cmd.exe
programmatically, apparently it parses its input
differently than when we use it in an interactive fashion.
I remember this problem drove me the up the freakin' wall when I first encountered
it roughly two years ago. With a lot of experimentation, I found the right
magic incantation:
Note that I quoted the whole command string another time! Now the executable actually starts. Let's verify this in the command prompt window: Yes, something like_snprintf(cmdbuf, sizeof(cmdbuf), "\"\"%s\" \"%s\"\"", exe, path); // originally: _snprintf(cmdbuf, sizeof(cmdbuf), "\"%s\" \"%s\"", exe, path);
cmd.exe /c ""c:\temp\foo bar\write.exe" "c:\temp\foo bar\foo bar.txt""
does what we want.
I was reminded of this weird behavior when John Scheffel, long-time user of our flagship
product OneSpace Designer Modeling and maintainer of the international
CoCreate user forum, reported funny
quoting problems when trying to run executables from our app's built-in Lisp interpreter.
John also found the solution and documented it in a Lisp version.
Our Lisp implementation provides a function called sd-sys-exec
, and you need to
invoke it thusly:
Kudos to John for figuring out the Lisp solution. Let's try to decipher all those quotes and backslashes in the(setf exe "c:/temp/foo bar/write.exe") (setf path "c:/temp/foo bar/foo bar.txt") (oli:sd-sys-exec (format nil "\"\"~A\" \"~A\"\"" exe path))
format
statement.
Originally, I modified his solution slightly
by using ~S
instead of ~A
in the format
call and thereby saving one level
of explicit quoting in the code:
(format nil "\"~S ~S\"" exe path))This is much easier on the eyes, yet I overlooked that the
~S
format specifier
not only produces enclosing quotes, but also escapes any backslash characters
in the argument that it processes. So if path
contains a backslash (not quite
unlikely on a Windows machine), the backslash will be doubled. This works
surprisingly well for some time, until you hit a UNC path which already starts
with two backslashes. As an example, \\backslash\lashes\back
turns into
\\\\backslash\\lashes\\back
, which no DOS shell will be able to grok anymore.
John spotted this issue as well. Maybe he should be writing these blog entries,
don't you think?
From those Lisp subtleties back to the original problem:
I never quite understood why the extra level of quoting is necessary for
cmd.exe
, but apparently, others have been in the same mess before. For example,
check out
this XEmacs code
to see how complex correct quoting can be. See also an online version of
the help pages for CMD.EXE
for more information on the involved quoting heuristics applied by the shell.
PS: A very similar situation occurs in OneSpace Designer Drafting as well
(which is our 2D CAD application). To start an executable write.exe
in a directory
c:\temp\foo bar
and have it open the text file c:\temp\foo bar\foo bar.txt
,
you'll need macro code like this:
Same procedure as above: If both the executable's path and the path of the data file contain blank characters, the whole command string which is passed down toLET Cmd '"C:\temp\foo bar\write.exe"' LET File '"C:\temp\foo bar\foo bar.txt"' LET Fullcmd (Cmd + " " + File) LET Fullcmd ('"' + Fullcmd + '"') { This is the important line } RUN Fullcmd
cmd.exe
needs to be enclosed in an additional
pair of quotes...
PS: See also http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
and http://daviddeley.com/autohotkey/parameters/parameters.htm
http://xkcd.com/1638/
-- ClausBrod - 27 Mar 2016
Let me explain: When we kicked off CoCreate as a company, we sat together and thought about awareness strategies for the new company. So we called our buddies from Microsoft and asked them to name some API functions after us, and in exchange we would port our software to Windows NT. Neat scheme, and as you discovered on your system, the cooperation between the two companies worked just fine. [... skipping explanation of the technical issue and hints on how to fix registry issue on the system ...] The next step for CoCreate towards world domination will be to talk to some of our buddies in, say, Portugal, and offer them to develop a Portugese version of our application if they name their country after us.Would I get away with a response like this if I was a support engineer? Maybe not. One more thing to like about being a software developer (Everybody in the newsgroup had a good chuckle back then.)
int fortytwo = 42; int shatter_illusions(void) { return fortytwo; } void quelle_surprise(void) { int fortytwo = 4711; printf("shatter_illusions returns %d\n", shatter_illusions()); }A seasoned C or C++ programmer will parse this code with his eyes shut and tell you immediately that
quelle_surprise
will print "42" because shatter_illusions()
refers to the global definition of fortytwo
.
Meanwhile, back in the parentheses jungle:
(defvar fortytwo 42) (defun shatter-illusions() fortytwo) (defun quelle-surprise() (let ((fortytwo 4711)) (format t "shatter-illusions returns ~A~%" (shatter-illusions))))To a C++ programmer, this looks like a verbatim transformation of the code above into Lisp syntax, and he will therefore assume that the code will still answer "42". But it doesn't:
quelle-surprise
thinks the right answer is "4711"!
Subtleties aside, the value of Lisp variables with lexical binding is determined
by the lexical structure of the code, i.e. how forms are nested in each other.
Most of the time, let
is used to establish a lexical binding for a variable.
Variables which are dynamically bound lead a more interesting life: Their
value is also determined by how forms call each other at runtime.
The defvar
statement above both binds fortytwo
to a value of 42 and declares the variable as
dynamic or special, i.e. as a variable with dynamic binding. Even if code
is executed which usually would bind the variable lexically, such as
a let
form, the variable will in fact retain its dynamic binding.
"Huh? What did you say?"
defvar
declares fortytwo
as dynamic and binds it to a value of 42.
let
statement in quelle-surprise
binds fortytwo
to a value of 4711,
but does not change the type of binding! Hence, fortytwo
still has dynamic binding which was previously established
by defvar
. This is true even though let
usually always creates
a lexical binding.
shatter-illusions
, when called, inherits the dynamic bindings of the
calling code; hence, fortytwo
will still have a value of 4711!
defvar
as follows:
(defmacro defvar (var &optional (form nil form-sp) doc-string) `(progn (si:make-special ',var) ,(if (and doc-string *include-documentation*) `(si:putprop ',var ,doc-string 'variable-documentation)) ,(if form-sp `(or (boundp ',var) (setq ,var ,form))) ',var))In the highlighted form, the variable
name
is declared as special,
which is equivalent with dynamic binding in Lisp.
This effect is quite surprising for a C++ programmer. I work with both Lisp and
C++, switching back and forth several times a day, so I try to minimize
the number of surprises a much as I can. Hence, I usually stay away from
special/dynamic Lisp variables, i.e. I tend to avoid defvar
and friends
and only use them where they are really required.
Unfortunately, defvar
and defparameter
are often recommended in Lisp
tutorials to declare global variables. Even in these enlightened
times, there's still an occasional need for a global variable, and if
you follow the usual examples out there, you'll be tempted to quickly add a
defvar
to get the job done. Except that now you've got a dynamically bound
variable without even really knowing it, and if you expected this variable
to behave like a global variable in C++, you're in for a surprise:
> (print fortytwo) 42 42 > (quelle-surprise) shatter-illusions returns 4711 NIL > (shatter-illusions) 42 > (print fortytwo) 42 42So you call
shatter-illusions
once through quelle-surprise
, and it tells
you that the value of the variable fortytwo
, which is supposedly global,
is 4711. And then you call the same function again, only directly, and it
will tell you that this time fortytwo
is 42.
The above code violates a very useful convention in Lisp programming which
suggests to mark global variables with asterisks
(*fortytwo*
). This, along with the guideline that global variables should
only be modified using setq
and setf
rather than let
, will avoid
most puzzling situations
like the above. Still, I have been confused by the dynamic "side-effect"
of global variables declared by defvar
often enough now that I made it
a habit to question any defvar
declarations I see in Lisp code.
More on avoiding global dynamic variables next time.
#import
to generate wrapper code for objects,
I recently tracked down a subtle reference-counting issue down to this single line:
IComponentArray *compArray = app->ILoadComponents();This code calls a method
ILoadComponents
on an application object which returns
an array of components. Innocent-looking as it is, this one-liner caused me
quite a bit of grief. If you can already explain what the reference counting
issue is, you shouldn't be wasting your time reading this blog. For the rest
of us, I'll try to dissect the problem.
(And for those who don't want to rely on my explanation: After I had learnt
enough about the problem so that I could intelligently feed Google with
search terms, I discovered a Microsoft
Knowledge Base
article on this very topic. However, even after reading the article, some details
were still unclear to me, especially since I don't live and breathe ATL all day.)
The #import
statement automatically generates COM wrapper functions. For
ILoadComponents
, the wrapper looks like this:
inline IComponentArrayPtr IApplication::ILoadComponents () { struct IComponentArray * _result = 0; HRESULT _hr = raw_ILoadComponents(&_result); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); return IComponentArrayPtr(_result, false); }
IComponentArrayPtr
is a typedef-ed template instance of
_com_ptr_t.
The constructor used in the code snippet above will only call AddRef
on the interface pointer if its second argument is true
. In our case, however,
the second arg is false
, so AddRef
will not be called. The IComponentArrayPtr
destructor, however, always calls Release()
.
Feeling uneasy already? Yeah, me too. But let's follow the course of action a little
bit longer. When returning from the wrapper function, the copy constructor of the
class will be called, and intermediate IComponentArrayPtr
objects will be
created. As those intermediate objects are destroyed, Release()
is called.
Now let us assume that the caller looks like above, i.e. we assign the return value
of the wrapper function to a CComPtr<IComponentArray>
type. The sequence
of events is as follows:
ILoadComponents
is called.
AddRef()
was called (at least)
once inside the server. The reference count is 1.
IComponentArrayPtr
smart pointer
object which simply copies the interface pointer value, but
does not call AddRef()
. The refcount is still 1.
CComPtr<IComponentArray> components = app->ILoadComponents();
ILoadComponents
returns an object of type IComponentArrayPtr
. At this
point, the reference count for the interface is 1 (see above). The
The compiler casts IComponentArrayPtr
to IComponentArray*
, then calls the
CComPtr
assignment operator which copies the pointer and calls AddRef
on it.
The refcount is now 2. At the completion of the statement, the temporary
IComponentArrayPtr
is destroyed and calls Release
on the interface. The
refcount is 1. Just perfect.
Now back to the original client code:
IComponentArray *compArray = app->ILoadComponents();Here, we assign to a "raw" interface pointer, rather than to a
CComPtr
,
When returning from the wrapper function,
the refcount for the interface is 1. The compiler casts IComponentArrayPtr
to IComponentArray*
and directly assigns the pointer. At the
end of the statement (i.e. the end of the "full expression"), the temporary
IComponentArrayPtr
is destroyed and calls Release
, decrementing the
refcount is 0. The object behind the interface pointer disappears, and
subsequent method calls on compArray
will fail miserably or crash!
So while ATL, in conjunction with the compiler's #import
support,
is doing its best to shield us from the perils of reference counting
bugs, it won't help us if someone pulls the plug from the ATL force-field
generator by incorrectly mixing smart and raw pointers.
This kind of reference counting bug would not have occurred if I had
used raw interface pointers throughout; the mismatch in calls to AddRef
and Release
would be readily apparent in such code. However, those
smart pointers are indeed really convenient in practice because
they make C++ COM code so much simpler to read. However, they do not
alleviate the programmer from learning about the intricacies of
reference counting. You better learn your IUnknown
before you do
CComPtr
.
This reminds me of Joel Spolsky's
The Perils of JavaSchools,
which is soooo 1990 (just like myself), but good fun to read.