DLL Memory Handling

From wiki.visual-prolog.com

Revision as of 22:21, 19 January 2008 by Thomas Linder Puls (talk | contribs) (c'a)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Heap memory that cross a DLL boundary should be handled with care, because the two sides may treat memory differently. In Visual Prolog a garbage collector handles the heap.

Vip/Vip

When the caller and callee are both in Visual Prolog (and compiled with the same version of Visual Prolog), then memory can simply be passed back and forth as you wish, because both caller and callee will use the same garbage collector for the heap management. So in this case the DLL boundary is completely transparent.

Notice: If caller and callee are compiled with different version of Visual Prolog, then they are foreign to each other and memory must be handled accordingly.

Foreign/Vip

Visual Prolog's garbage collector does not know whether a foreign program uses a piece of memory or not, and therefore it may reclaim the memory even though the foreign part of the program still needs it (most likely this will result in an access violation at some later point). Therefore you must decide on a memory handling scheme. Basically there are two schemes to choose.

Caller provides memory

This scheme is used in most Windows API calls and in most C/C++ context. The principle is that the caller allocates the memory and the callee copies the result into that memory.

In you getString case the vip declaration would look like this:

class dllExport
predicates
    getString : (string ResultBuffer) language stdcall.
end class dllExport

Notice that the string parameter is an input argument. This is because the caller must provide a buffer for the result. (Very often there would also be another argument stating the size of the buffer, such that the DLL will not write outside the buffer).

The corresponding implementation could look like this:

implement dllExport
clauses
    getString(Buffer) :-
        Result = "ResultString",
        ByteCount = sizeOfDomain(char) * (sting::length(Result)+1),
        memory::copy(uncheckedConvert(pointer, Buffer), uncheckedConvert(pointer, Result), ByteCount).
end implement dllExport

The last two lines in the clause should be turned into a separate predicate (in fact, it should ought to be in PFC already, but I can't seem to find it).

Callee provides a clean-up routine

An alternative approach is to allow callee memory to float into the caller, and then give the caller a predicate to call once the memory should be released.

In the getString example it could look like this:

class dllExport
predicates
    getString : () -> string ResultBuffer language stdcall.
predicates
    releaseString : (string ToRelease) language stdcall.
end class dllExport

In this case the caller will simply receive the string from the DLL, but once the caller is done with the string it must call releaseString to have it released.

In the implementation we have to make sure that the memory is not garbage collected until releaseString has been called. We can achieve this by asserting the string in a class fact and retract it when it is released, because memory that can be reached from a class fact is never garbage collected.

implement dllExport
class facts
    keepAlive : (pointer ToKeepAlive).
clauses
    getString() Result :-
        Result = "ResultString",
        assert(keepAlive(uncheckedConvert(pointer, Result))).
clauses
    releaseString(ToRelease) :-
        retractAll(keepAlive(uncheckedConvert(pointer, ToRelease))).
end implement dllExport

The reason that I assert pointers and not strings, is because otherwise the retract might retract a wrong string, because strings are retracted based on their value (i.e. the characters in the string). Pointers are also retracted based on their value, but their value is a memory address.