DLL Memory Handling
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.
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.