Calling a DLL Under Windows

<< >> Contents Help

# 21. Calling a DLL Under Windows

Interfacing to a DLL is one thing you can do the old-fashioned way: by getting a copy of a working program and editing it. You can find starter programs in the J distribution. A good one to start with is \system\packages\winapi\registry.ijs. You can glance through this program to see what it does. It starts with

require 'dll'

which defines the verb cd . Calls to the DLL appear in lines like

rc =. 'Advapi32 RegCreateKeyExA i i *c i *c i i i *i *i'

cd root;key;0;'';0;sam;0;(,_1);(,_1)

(this is a single line in the program; I have shown it here as 2 lines because it won't fit on the page as a single line)

This example exhibits the elements of a call to a DLL. The left operand of cd is a character list describing the function to be called and what arguments it is expecting. Here we are calling the entry point RegCreateKeyExA in the library Advapi32. The sequence of is and *cs describes the interface to the function. The first item in that sequence describes the type of value returned by the function; the other items are the arguments to the function and are a one-for-one rendering of the argument list that would be passed in C. So, the line above is appropriate for calling a function defined with the prototype

int RegCreateKeyExA(int, char *, int , char *, int, int, int, int *, int *);

The descriptors can be c (char), s (short) , i (int), f (float), d (double), j (complex), or n (placeholder—a value of 0 is used and the result is ignored); all but n can be preceded by * to indicate an array of that type. * by itself indicates an array of unspecified type.

The right operand of cd is the actual arguments to be passed to the called function. It must be a list of boxes in which the contents of each box holds one argument to the called function (there is no box to correspond to the returned value). An argument whose description does not include * must correspond to a boxed scalar of the appropriate type. An argument described with * must correspond to a box whose contents are either an array or a boxed memory address (see below). The address of the array or memory area is passed to the DLL and the DLL may modify the array or memory area; this is how you return an array from the function.

s and f are not native J types. A scalar argument described by s or f is converted by the interpreter to the correct form, while an argument described by *s or *f is assumed to point to a memory area with the correct format.

i and d descriptors pose a bit of a problem. They both correspond to numbers in J, but unfortunately it is up to you to ensure that the J internal representation of the number matches the type expected by the DLL. There is no officially-sanctioned way to do this, but as of J5.02 you can use monad <. to ensure that a value is an integer (for i) and monad _&<. to ensure that a value is floating-point (for d).

Note that if you get things wrong and the function scribbles outside the bounds of an array, J may crash. Note also that a vector of 0s and 1s is held inside J as a vector of Booleans, which are chars. When the function calls for a vector of ints, J will convert any Boolean to int. In the example above, (,_1) reserved space for an int; (,0) would have worked too but would require a conversion.

When the function returns, its returned value will be boxed and appended to the front of the boxed list that was the right operand of cd to produce the result of the execution of cd . You may use this returned value as you see fit. Any box that contained an array may have had its contents modified by the function; you may open the box to get the changed value.

If J was unable to call the DLL, the cd verb fails with a domain error. You can then execute the sentence cder '' which will return a 2-element list indicating what went wrong. The User Guide gives a complete list of errors; the most likely ones are 4 0 (the number of arguments did not match the number of declarations), 5 x (declaration number x was invalid—the count starts with the declaration of the returned value which is number 0), and 6 x (argument number x did not match its declaration—the count starts with the first argument which is number 0 and must match the second declaration).

Memory Management

Passing arrays into the called function is adequate only for simple functions. If the function expects an argument to be a structure, possibly containing pointers to other structures, you will have to allocate memory for the structures, fill the structures appropriately, and free the memory when it is no longer needed. J provides a set of verbs to support such memory management.

Allocate memory: mema

mema length allocates a memory area of length bytes. The result is the address of the memory area, as an integer. It is 0 if the allocation failed. mema has infinite rank.

You must box the memory address before using it as an operand to cd . Do not box the address for use as an operand to memf, memw, or memr .

Free memory: memf

memf address frees the memory area pointed to by address . address must be a value that was returned by mema . Result of 0 means success, 1 means failure. memf has infinite rank.

Write Into a Memory Area: memw

data memw address,byteoffset,count,type
causes data to be written, starting at an offset of byteoffset from the area pointed to by address, for a length of count items whose type is given by type . type is 2 for characters, 4 for integers, 8 for floating-point numbers, 16 for complex numbers; if omitted, the default is 2 . If type is 2, count may be one more than the length of data to cause a string-terminating NUL (\0) to be written after the data .

Read From a Memory Area: memr

memr address,byteoffset,count,type
produces as its result the information starting at an offset of byteoffset from the area pointed to by address, for a length of count items whose type is given by type . type is 2 for characters, 4 for integers, 8 for floating-point numbers, 16 for complex numbers; if omitted, the default is 2 . The result is a list with count items of the type given by type .If type is 2, count may be _1 which causes the read to be terminated before the first NUL (\0) character encountered.

Aliasing of Variables

When a noun is assigned the value of another noun, as in

a =. b =. 5

a single memory area is used to hold the value common to both nouns, and the two nouns are said to be aliases of each other. Aliasing obviously reduces the time and space used by a computation. The interpreter takes care to ensure that aliasing is invisible to the programmer; if after the statement above we execute

b =. 6

the interpreter will assign the new value to b only, leaving a unchanged. What actually happens is that the new value is created in a data block of its own and the descriptor for the noun b is changed to point to the new block. (Almost all verbs create their outputs in newly-allocated data blocks. As of release 5.03 the exceptions are ], [, and , and u} when used in one of the forms that produces in-place modification. Increasing the number of cases recognized for in-place execution is a continuing activity of the J developers).

If there were nothing more to say about aliasing, I would not single it out for mention from among the dozens of performance-improving tricks used by the interpreter. What makes it worth considering is the effect aliasing has when elements outside the J language touch J's nouns. This can occur in two ways: when a noun is mapped to a file and when a noun is modified by a DLL.

Aliasing of Mapped Nouns

When a noun is mapped to a file, the descriptor for the noun points to the file's data and that pointer is never changed even if a value is assigned to the variable: the whole point of mapping the noun to the file is to cause changes in the noun to be reflected in the file, so any assignment to the noun causes the data to be copied into the area that is mapped to the file.

In addition, when b is a noun mapped to a file and is assigned to another noun as with

a =. b

the noun a, which is aliased to b, also inherits the 'mapped-to-file' attribute. This behavior is necessary to make mapped files useful, because assignments to x. and y. are implicit whenever a verb is invoked and it would defeat the whole purpose of mapping if the data of the file had to be copied every time the mapped noun was passed to a verb. The combination of aliasing and mapping means that any assignment to a mapped noun also changes the values of all other nouns which share the same mapping: for example, if you pass a mapped noun a as the right operand of a verb that modifies its y., y., a, and the data in the file will all be modified.

Keeping track of the aliasing is the price you pay for using mapped files. If you need to copy a noun making sure you get a fresh, unmapped data block, you must not assign the mapped noun directly, but instead assign the result of some verb that creates its output in a new data block. For example, as of release 5.01 the assignment

a =. 1&# b

will create a new data block containing the data of b, and a will point to that new block.

Aliasing of DLL Operands

The J interpreter uses aliasing for boxed cells of an array, so that if you execute

b =. i. 10000 10000

a =. b;5

item 0 of a simply contains a pointer to the data block of b rather than a fresh copy of the 400MB array. In addition, when a list of boxes is used as the right operand of cd, as in

'dll-spec' cd root;key;0;'';0;sam;0;(,_1);(,_1)

any array operands to the DLL function are passed via a pointer to the data in the boxed list, with no separate copy of the data being made. This means that if the DLL modifies one of its arguments, any nouns aliased to that argument will also be modified: if the DLL function called above modifies its argument 1, the noun key and any noun aliased to key (possibly including private nouns in suspended verbs) will be changed. To protect yourself from such side-effects, you can use (1#key) in place of key in the invocation of cd .

<< >> Contents Help