views:

536

answers:

7

I have to create a DLL which is used by a VB6 application. This DLL has to provide several functions, some of them must return strings.

This is the VB6 declaration:

Declare Function MyProc Lib "mylib.dll" (ByVal Param As String) As String

And this the Delphi implementation stub in mylib.dll:

function MyProc(AParam: PChar): PChar; stdcall;
var
  ReturnValue: string;
begin
  ReturnValue := GetReturnValue(AParam);
  Result := ???;
end;

What do I have to return here? Who will free the memory of the returnd PChar string?

EDIT: I'm asking about Delphi 2005 (PChar = PAnsiChar)

A: 

Use the Windows API to allocate the memory that the PChar pointer points into. Then, the VB app can deallocate the memory after use, using the Windows API, too.

Lars D
This binds you to a particular implementation (read: memory allocation strategy). Providing coupled allocate/deallocae routines is more transparent.
Konrad Rudolph
+4  A: 

You need to craft a BSTR instead. VB6 strings are actually BSTRs. Call SysAllocString() on the Delphi side and return the BSTR to the VB6 side. The VB6 side will have to call SysFreeString() to free the string - it will do it automatically.

If PChar corresponds to an ANSI string (your case) you have to manually convert it to Unicode - use MultiByteToWideChar() for that. See this answer for how to better use SysAllocStringLen() and MultiByteToWideChar() together.

sharptooth
I did some experiments before asking on SO. I used a global (Ansi)String variable and just returned PAnsiChar(MyGlobalVar) and it worked. Why did that work when in fact I must return a BSTR (=two bytes per character)?
DR
AFAIK when it comes to API calls, VB6 implicitly casts all strings to ANSI strings, even though VB6 works with BSTRs internally.
DR
DR is correct - if you use ByVal in the DLL declaration, VB6 will implicitly convert all strings to and from ANSI strings when calling DLLs.
MarkJ
I've just read this: http://vb.mvps.org/tips/vb5dll.asp . Now I don't understand anything anymore... Sometimes the strings get converted, sometimes not, but they are always BSTRs... ?!?!
DR
According to that black magic text, you can just do as they suggest in UpperCaseByVal() function.
sharptooth
If you use ByVal, VB6 converts the strings to/from ANSI and passes the DLL a pointer to the ANSI string. The Microsoft doc does say that, but in a rather confusing way. Focus on `When you pass a string ByVal, Visual Basic passes a pointer to the beginning of the string data`
MarkJ
+1  A: 

I would say that whoever allocates the memory must also free it in this case. You will run into problems with other scenarios. So the most safe and clean way would be:

  1. The DLL allocates memory (because it knows how much) and returns the PChar to caller
  2. After the caller is done with it, it calls FreePointer back to the DLL
  3. DLL frees the memory in the FreePointer exported function

The setup would be like this:

unit DLL;

interface

uses
  SysUtils;

function Execute(const Params: PChar): PChar; stdcall;
procedure FreePointer(const P: PChar); stdcall;

exports Execute;
exports FreePointer;

implementation

function Execute(const Params: PChar): PChar; stdcall;
var
  Size: Cardinal;
begin
  Size := Calculate the size;
  GetMem(Result, Size);

  ...do something to fill the buffer
end;

procedure FreePointer(const P: PChar); stdcall;
begin
  FreeMem(P);
end;

end.
Runner
As far as I understand this is not how VB6/COM works. With BSTRs the caller is responsible for freeing *all* strings, as weird as this might sound. See this SO question: http://stackoverflow.com/questions/872794/who-owns-returned-bstr
DR
Oh, din't know that. I don't know VB very well. Yes it is a little confusing if you ask me.
Runner
This would work, but I want to be able to use the DLL function as I would use any other VB6 function. An additional FreePointer call after every call to my function would mess up my code. I also fear that with this the internal string handling functions of VB6 might interfere.
DR
Understood. I found that the cleanest way in Delphi. But I understand your point.
Runner
@DR The caller is responsible for freeing all strings *under COM*. Your DLL is not a COM DLL so that other SO question doesn't apply.
MarkJ
+1  A: 

Combining Sharptooth and Lars D's answer; aren't widestrings already allocated via windows and BSTR?

Marco van de Voort
I should have mentioned: Delphi 2005
DR
widestrings are afaik D4+, so no problem there.
Marco van de Voort
Won't Delphi free the WideString automatically? Anyway, I went with Sharptooth's answer, because as noted in several answers and comments, VB6 packs an ANSI string into a BSTR, which would result in ugly casts on the Delphi side.
DR
A: 

I'm not familiar with Dephi, but here are the two main options when using strings with a non-COM DLL and VB6.

Option 1. Use "ANSI" strings.

'DLL routine expecting to be passed pointers to ANSI strings '
'VB6 will allocate and deallocate the strings '
'Its vital that VB6 allocates sufficient space for the return string '
Declare Sub MyProc Lib "mylib.dll" (ByVal Param As String, _ 
  ByVal OutVal As String) 

Function DoMyProc(ByVal Param As String) As String
  Dim sResult As String
  sResult = Space$(255)  ' create 255 bytes of space for the return string '
  Call MyProc(Param, sResult) 
  DoMyProc = sResult
End Function

Option two. Use BSTRs.

'DLL routine expecting to be passed two BSTRs. It will modify the second one. '
'VB6 "owns" both BSTRs and will deallocate them when it has finished with them. '
Declare Sub MyProc(ByVal lpParam As Long, ByVal lpOutVal As Long)

Function DoMyProc(ByVal Param As String) As String
  Dim sResult As String
  Call MyProc(StrPtr(Param), StrPtr(sResult)) 
  DoMyProc = sResult
End Function

I'd also suggest looking at the Microsoft advice on writing C DLLs to be called from VB. Originally released with VB5 but still relevant to VB6.

MarkJ
Perhaps a better reference is the *Hardcore Visual Basic* section on passing strings to API calls (your Delphi DLL is like an API call from the VB6 point of view) http://vb.mvps.org/hardcore/html/stringsinsideout.htm
MarkJ
A: 

You cannot return a PChar as a function result, but you can pass an additional PChar parameter and copy the string you want to return to this PChar. Note, that VB must allocate that string to the required size before passing it to the dll. Also in VB that parameter must be declared as byval param as string AND it must be passed with byval:

  param = "aaaaaaaaaaaaaaaaaaaa" ' reserve 20 characters
  call myproc(byval param)

The additional byval in the call will do the compiler magic of converting a VB string to a PChar and back.

(I hope I remember this is correctly, it has been quite a while since I was forced to use VB.)

dummzeuch
Is this restriction that you cannot return PChar as a function result, VB specific?
Runner
I don't think this is true. There's many MSDN documentation on this topic, I just don't seem to understand it.
DR
Did you try my aproach? Allocating and dealocating memory inside your DLL. Or is the problem actually in the format of the returned memory (PChar convention or something similar)?My code is tested in BDS 2006 and RAD 2010 and works perfectly.
Runner
@runner: VB does not have a PChar type but can only convert its own string type to PChar and back. So there is no way you could call a FreeMemory function from VB (OK, there might be some hack to do it).(I am talking about VB6 like in the original question, I have no idea about VB.NET in that respect.)
dummzeuch
+2  A: 

If you don't want to risk crashes or memory leaks, then craft your API using the Windows API as a model. There, the API functions generally don't allocate their own memory. Instead, the caller passes a buffer and tells the API how big the buffer is. The API fills the buffer up to that limit. See the GetWindowText function, for example. Functions don't return pointers, unless they're pointers to things the caller already provided. Instead, the caller provides everything itself, and the function just uses whatever it's given. You almost never see an output buffer parameter that isn't accompanied by another parameter telling the buffer's size.

A further enhancement you can make to that technique is to allow the function to tell the caller how big the buffer needs to be. When the input pointer is a null pointer, then the function can return how many bytes the caller needs to provide. The caller will call the function twice.

You don't need to derive your API from scratch. Use already-working APIs as examples for how to expose your own.

Rob Kennedy
Good answer, +1. How to return strings is a topic that seems to be discussed over and over again, albeit in variations and with different languages. I wish there was an answer that could be pointed to for reference. This is at least a start towards such an answer.
mghie