This article explains how to pass strings between a C# assembly and an unmanaged C++ DLL.
Background
The reader should have a basic knowledge of C# and unmanaged C++.
Returning a BSTR
An easy way to return a string
from an unmanaged C++ DLL is to use the BSTR
type.
The following C++ export returns a BSTR
containing a version string
:
extern BSTR __stdcall GetVersionBSTR()
{
return SysAllocString(L"Version 3.1.2");
}
The .DEF file is as follows:
LIBRARY
EXPORTS
GetVersionBSTR
The export is imported into the .NET application as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.BSTR)]
public static extern string GetVersionBSTR();
}
}
The managed code invokes the imported function as follows:
string version = Model.ImportLibrary.GetVersionBSTR();
The managed code marshals the string
as a BSTR
and frees the memory when it is no longer required.
When calling the export from unmanaged code, the BSTR
should be freed, and a failure to do so creates a memory leak.
Returning a char *
Marshalling a char *
return value is more difficult for the simple reason that the .NET application has no idea how the memory was allocated, and hence it does not know how to free it. The safe approach is to treat the return char *
as a pointer to a memory location. The .NET application will not try to free the memory. This of course has the potential for memory leaks if the managed code allocated the string
on the heap.
The following C++ export returns a version string
defined as a string
literal:
extern char * __stdcall GetVersionCharPtr()
{
return "Version 3.1.2";
}
The corresponding .DEF file is as follows:
LIBRARY
EXPORTS
GetVersionCharPtr
The export is imported into the .NET application as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetVersionCharPtr();
}
}
The managed code invokes the imported function as follows:
IntPtr intPtr = Model.ImportLibrary.GetVersionCharPtr();
string version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(intPtr);
Passing a String as a BSTR Parameter
It is very easy to pass a string
as a parameter using the BSTR
type.
The following C++ export takes a BSTR
parameter:
extern void __stdcall SetVersionBSTR(BSTR version)
{
}
The unmanaged code should not free the BSTR
.
The .DEF file is as follows:
LIBRARY
EXPORTS
SetVersionBSTR
This function is imported into a C# application as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersionBSTR
([MarshalAs(UnmanagedType.BSTR) string version);
}
}
The managed code invokes the imported function as follows:
Model.ImportLibrary.SetVersionBSTR("Version 1.0.0);
Passing a String as a char * Parameter
The following C++ export takes a char *
parameter:
extern void __stdcall SetVersionCharPtr(char *version)
{
}
The .DEF file is as follows:
LIBRARY
EXPORTS
SetVersionCharPtr
This function is imported into a C# application as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersionCharPtr
([MarshalAs(UnmanagedType.LPStr) string version);
}
}
The managed code invokes the imported function as follows:
Model.ImportLibrary.SetVersionCharPtr("Version 1.0.0);
Returning a String with a BSTR * Parameter
An unmanaged C++ DLL can return a string
to the caller using a BSTR *
parameter. The DLL allocates the BSTR
, and the caller frees it.
The following C++ export returns a string
using a parameter of BSTR *
type:
extern HRESULT __stdcall GetVersionBSTRPtr(BSTR *version)
{
*version = SysAllocString(L"Version 1.0.0");
return S_OK;
}
The .DEF file is as follows:
LIBRARY
EXPORTS
GetVersionBSTRPtr
This function is imported into a C# application as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern int GetVersionBSTRPtr
([MarshalAs(UnmanagedType.BSTR) out string version);
}
}
Using this function in the C# code is straightforward:
string version;
Model.ImportLibrary.GetVersionBSTRPtr(out version);
The managed code will automatically take care of the memory management.
Passing a String as a char** Parameter
The following C++ export returns a version string
using a char **
parameter:
extern HRESULT __stdcall GetVersionCharPtrPtr(char **version)
{
*version = "Version 1.0.0";
return S_OK;
}
The .DEF file is as follows:
LIBRARY
EXPORTS
GetVersionCharPtrPtr
The function is imported into the managed code as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void GetVersionCharPtrPtr(out IntPtr version);
}
}
Using this function in the C# code is straightforward:
IntPtr intPtr;
Model.ImportLibrary.GetVersionCharPtrPtr(out intPtr);
string version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(intPtr);
Clearly, there is a danger of memory leaks if the unmanaged DLL allocates the memory for the string
on the heap.
Passing a String with a Buffer
A safe way to return a string
from an unmanaged C++ DLL is to use a buffer allocated by the caller. For example:
extern void __stdcall GetVersionBuffer(char *buffer, unsigned long *pSize)
{
if (pSize == nullptr)
{
return;
}
static char *version = "Version 5.1.1";
unsigned long size = strlen(version) + 1;
if ((buffer != nullptr) && (*pSize >= size))
{
strcpy_s(buffer, size, s_lastSetVersion);
}
*pSize = size;
}
The caller should call the function twice, once with a null
buffer address to determine the required buffer size, and then with an appropriately sized buffer.
The .DEF file is as follows:
LIBRARY
EXPORTS
GetVersionBuffer
The function is imported into the managed code as follows:
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi)]
public static extern Boolean GetVersionBuffer
([MarshalAs(UnmanagedType.LPStr)] StringBuilder version, ref UInt32 size);
Using this function in the C# code is straightforward:
UInt32 size = 0;
Model.ImportLibrary.GetVersionBuffer(null, ref size);
var sb = new StringBuilder((int)size);
Model.ImportLibrary.GetVersionBuffer(sb, ref size);
string version = sb.ToString();
The above code determines the required buffer size, and then retrieves the version string
.
Passing an Array of Strings
It is surprisingly easy to pass an array of string
s using the .NET array and C++ SAFEARRAY
types.
The following C++ export takes a SAFEARRAY
parameter containing an array of BSTR
values:
extern void __stdcall SetStringArray(SAFEARRAY& safeArray)
{
if (safeArray.cDims == 1)
{
if ((safeArray.fFeatures & FADF_BSTR) == FADF_BSTR)
{
BSTR* bstrArray;
HRESULT hr = SafeArrayAccessData(&safeArray, (void**)&bstrArray);
long iMin = 0;
SafeArrayGetLBound(&safeArray, 1, &iMin);
long iMax = 0;
SafeArrayGetUBound(&safeArray, 1, &iMax);
for (long i = iMin; i <= iMax; ++i)
{
}
}
}
}
The .DEF file is as follows:
LIBRARY
EXPORTS
SetStringArray
The function is imported into the managed code as follows:
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void SetStringArray
([MarshalAs(UnmanagedType.SafeArray)] string[] array);
Using this function in the C# code is simple:
string[] array = new string[4] {"one", "two", "three", "four"};
Model.ImportLibrary.SetStringArray(array);
Although the C++ code is a little bit messy, the managed code could not be simpler.
Returning an Array of Strings
The following C++ export fills a SAFEARRAY
parameter with an array of BSTR
values:
extern void __stdcall GetStringArray(SAFEARRAY *&pSafeArray)
{
if (s_strings.size() > 0)
{
SAFEARRAYBOUND Bound;
Bound.lLbound = 0;
Bound.cElements = s_strings.size();
pSafeArray = SafeArrayCreate(VT_BSTR, 1, &Bound);
BSTR *pData;
HRESULT hr = SafeArrayAccessData(pSafeArray, (void **)&pData);
if (SUCCEEDED(hr))
{
for (DWORD i = 0; i < s_strings.size(); i++)
{
*pData++ = SysAllocString(s_strings[i].c_str());
}
SafeArrayUnaccessData(pSafeArray);
}
}
else
{
pSafeArray = nullptr;
}
}
The s_strings
variable is assumed to be a std::list<std::string>
instance containing multiple entries.
The .DEF file is as follows:
LIBRARY
EXPORTS
GetStringArray
The function is imported into the managed code as follows:
[DllImport(DLL_LOCATION, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern void GetStringArray
([MarshalAs(UnmanagedType.SafeArray)] out string[] array);
This is almost the same as for the SetStringArray
method except that the argument is declared as the 'out
' parameter.
The function may be called from the C# code as follows:
string[] array;
Model.ImportLibrary.GetStringArray(array);
As before, the C++ code is a bit messy, but the managed code could not be simpler.
Dealing with ASCII and Unicode Strings
Quite often, a DLL will define ASCII and Unicode versions of a function. Indeed Microsoft often do this. The unmanaged MessageBox
function is actually defined in a Windows header file as follows:
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
Fortunately, the .NET Framework has built in support for this.
We could have defined our first export as follows:
extern void __stdcall SetVersionA(char *version)
{
}
extern void __stdcall SetVersionW(wchar_t *version)
{
}
The .DEF is defined as follows:
LIBRARY
EXPORTS
SetVersionA
SetVersionW
The function is imported into C# managed code as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, CharSet = CharSet.Unicode,
CallingConvention = CallingConvention.StdCall)]
public static extern string SetVersion(string version);
}
}
Note that the function name is declared as SetVersion
, rather than SetVersionA
or SetVersionW
, and the CharSet
field is set to Unicode
.
Using this function in the C# code is straightforward:
string version = "Version 3.4.5"
Model.ImportLibrary.SetVersion(version);
If you debug through the above code, you will see that the SetVersionW
export is invoked. This is because the CharSet
was set to Unicode
. If you change the CharSet
to Ansi
, and debug through, lo and behold, the SetVersionA
export is invoked!
We can easily disable this feature using the ExactSpelling
field as follows:
namespace DemoApp.Model
{
static class ImportLibrary
{
const String DLL_LOCATION = "DemoLibrary.dll";
[DllImport(DLL_LOCATION, ExactSpelling = true,
CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern void SetVersion(string version);
}
}
Now the .NET application will try and invoke a function called SetVersion
. Since one does not exist, the function call will fail.
Conclusions
Passing a string
into an unmanaged C++ DLL is very easy. Returning a string
is not so easy, and pitfalls include memory leaks and heap corruption. A simple way is for the caller to allocate a buffer of the required size. This method is suitable for both managed and unmanaged clients. A slightly easier alternative is to use the BSTR *
type, with the risk that an unmanaged client could introduce a memory leak by not freeing the BSTR
.
Passing an array of string
s between a managed application and an unmanaged DLL is also fairly easy, although the code in the unmanaged DLL is a little messy.
I have by no means exhausted the ways to exchange string
s between managed and unmanaged code. Other methods are left as an exercise for the reader.
Example Code
I have created a simple WPF and C++ DLL application which demonstrates the ideas discussed in this article. Don't worry if you do not understand WPF, you shouldn't have any trouble understanding the relevant code fragments, and with a bit of luck, you will be encouraged to go on and learn WPF, which I highly recommend.
History
- 7th July, 2017: First version