//
you're reading...
Programming Issues/Tips

Writing to Data Exported From a DLL in Managed Code.

1. Introduction.

1.1 This article is a direct follow-up from my last blog entry Accessing Exported Data From a DLL in Managed Code in which I expounded on the basic principles behind how to access data items exported from a DLL.

1.2 In that article, I wrote that besides being able to read them, it is also possible to write values into global data exported from DLLs.

1.3 I had initially wanted to include code in that article for writing to exported data. However, as I experimented and researched more and more, I accumulated quite alot of code that would render the first article rather long. I eventually decided that writing a new article would be a better choice.

1.4 Also included in this article are additional exported data reading code that I had also left out in the first article.

2. Writing to Exported Data in Managed Code.

2.1 In the context of managed code, writing to data exported from a DLL can be both simple and complicated, depending on how one looks at the process.

2.2 On the one hand, it is simple because all that matters is writing the correct bytes to a fixed memory location.

2.3 On the other hand, it is complicated for the following reasons :

  • The binary data that are stored in the memory locations are not easy to manipulate using managed code and would need to be first translated into managed objects.
  • Only then can the managed objects (which represent these data) be modified easily.
  • However, once translated into a managed object, the original exported data is detached from its managed representation.
  • Modifications to its managed representation does not result in its own change.
  • After being modified, the data of the managed representation will need to be written to its associated exported data memory location.
  • Except for blittable types, it can sometimes be difficult and complicated to serialize managed data objects into binary data to be inserted into these memory locations.
  • Alot depends on the type of each exported data.

2.4 Contrast this to C++ coding where exported DLL data can be imported (via __declspec(dllimport)), changed and updated rather easily. This is due to the low-level nature of the C++ language.

2.5 It is the fact that the original unmanaged data types can have vastly different memory representations in the managed world that makes life complicated.

2.6 In this sense, there is no generic way to read and write exported data except for blittable types and structures whose managed counterparts have members which have been adequately decorated with MarshalAsAttrributes.

2.7 The developer has to carefully analyze the type of each exported data and formulate the appropriate way to read and write it. 

3. Sample DLL that Exports Data.

3.1 In this section, I shall present some C++ code in which some data are exported :

// TestDLL.cpp : Defines the exported functions for the DLL application.
//

#include "stdafx.h"
#include 

extern "C" int iCExportedInteger = 100;

#pragma pack (1)
struct MyStructureTag
{
  char	m_szString[80];
  int	m_iInteger;
};

extern "C" MyStructureTag MyStructure = { "MyStructure", 200 };

extern "C" char szExportedString[256] = "Exported string.";

extern "C" wchar_t wszExportedString[256] = L"Wide exported string.";

extern "C" BSTR bstrExported = NULL;

The above source code is part of a DLL project which compiles to TestDLL.dll. The following is a summary of the exported data :

  • All data are declared extern “C” and are exported using the .DEF file. This is done for simplicity : to avoid name mangling.
  • iCExportedInteger is a 32-bit integer which is fully compatible with the .NET Int32 type. The Int32 type is a blittable type and so iCExportedInteger will have a managed representation that can easily update it.
  • MyStructure is a structure of type MyStructureTag. In order to have a managed representation for it, a managed version of the MyStructureTag structure must be defined in managed code.
  • szExportedString and wszExportedString  are both buffers of capacity 256. It is customary to use such buffers to hold C-style NULL-terminated strings. The former is used for holding an ANSI string while the latter is used for unicode strings. Both types of strings are represented as .NET strings in managed code.
  • Because both buffers are of fixed character lengths, their managed representations may be modified (for later update) as long as the modified strings remain within 255 characters (the last character place in each buffer must be reserved for the NULL character).
  • bstrExported is a BSTR which is also represented in managed code as a string. However, unlike szExportedString and wszExportedString, bstrExported cannot be treated as a character buffer. I shall expound on this further when we study how to write to this exported data.

3.2 I have used typical data types for demonstration purposes and it is the basic principles of writing to these memory locations that I aim to expound on.

3.3 After explaining the fundamentals, it is my hope that the reader will self-experiment reading and writing various data types exported from a DLL.

3.4 Beginning from the next section, I shall expound on the way to read and write to each of the exported data of TestDLL.dll in its own section.

4. Writing to iCExportedInteger – a 32-bit Integer.

4.1 The following sample code demonstrates how this can be done :

[DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);

[DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

[DllImport("Kernel32.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);

private static T GetExportedData<T>(IntPtr pExportedDataAddress) where T : struct
{
    return (T)Marshal.PtrToStructure(pExportedDataAddress, typeof(T));
}

private static void UpdateExportedData<T>(IntPtr pExportedDataAddress, T value) where T : struct
{
    Marshal.StructureToPtr(value, pExportedDataAddress, false);
}

private static void TestLoadAndUpdateExportedIntegerData()
{
    // Load the DLL.
    IntPtr hDLL = LoadLibrary("TestDLL.dll");

    // Perform action only if we are able to load it.
    if (hDLL != IntPtr.Zero)
    {
        // Obtain the runtime address of the exported data with name "iCExportedInteger".
        IntPtr piCExportedInteger = (IntPtr)GetProcAddress(hDLL, "iCExportedInteger");
        // Obtain the value of the data.
        Int32 iCExportedInteger = GetExportedData<Int32>(piCExportedInteger);
        // Display the data on the console output.
        Console.WriteLine("iCExportedInteger : {0:D}.", iCExportedInteger);

        // Change the value of the exported data.
        UpdateExportedData(piCExportedInteger, 500);

        // Re-obtain the value of the exported data.
        iCExportedInteger = GetExportedData<Int32>(piCExportedInteger);
        // Display the updated data on the console output.
        Console.WriteLine("iCExportedInteger : {0:D}.", iCExportedInteger);

        // Finally, free the library when it is no longer required.
        FreeLibrary(hDLL);
        hDLL = IntPtr.Zero;
    }
}

The following is a summary of the code above :

  • For reading from the exported data memory location, Marshal.PtrToStructure() is used.
  • For writing to the exported data, Marshal.StructureToPtr() is used.

4.2 As can be seen from the above code, reading and writing to a 32-bit integer is be very simple indeed. This simplicity, in fact, applies to all blittable types which include integers of various lengths and floating point numbers.

4.3 Given the genericity of reading and writing blittable types, I created 2 generic functions GetExportedData<T>() and UpdateExportedData<T>() for this purpose.

4.4 Notice the “where” constraint used (“where T : struct”) which limits the generic parameter T to value types. As will be seen later on the GetExportedData<T>() and UpdateExportedData<T>() generic functions can also be used on structures.

5. Writing to MyStructureTag – A Structure.

5.1 An unmanaged structure requires a managed version if it is to be used in the .NET environment.

5.2 The MyStructureTag structure has the following managed counterpart :

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
struct MyStructureTag
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
    public string m_szString;
    public Int32 m_iInteger;
};

5.3 Note the use of various attributes for this structure. These are essential in ensuring that it is compatible with its unmanaged counterpart (listed in point 3.1).

5.4 The code below shows how this structure is read from exported DLL data, updated and then written back to the DLL :

private static void TestLoadAndUpdateExportedStructureData()
{
    // Load the DLL.
    IntPtr hDLL = LoadLibrary("TestDLL.dll");

    // Perform action only if we are able to load it.
    if (hDLL != IntPtr.Zero)
    {
        // Obtain the runtime address of the exported data with name "MyStructure".
        IntPtr pMyStructure = (IntPtr)GetProcAddress(hDLL, "MyStructure");
        // Obtain the value of the data.
        MyStructureTag MyStructure = GetExportedData<MyStructureTag>(pMyStructure);
        // Display the data on the console output.
        Console.WriteLine("MyStructure.m_szString : {0:S}.", MyStructure.m_szString);
        Console.WriteLine("MyStructure.m_iInteger : {0:D}.", MyStructure.m_iInteger);

        // Change the value of the exported data.
        MyStructure.m_szString = "My .NET Structure";
        MyStructure.m_iInteger = 300;
        UpdateExportedData(pMyStructure, MyStructure);

        // Re-obtain the value of the exported data.
        MyStructure = GetExportedData<MyStructureTag>(pMyStructure);
        // Display the updated data on the console output.
        Console.WriteLine("MyStructure.m_szString : {0:S}.", MyStructure.m_szString);
        Console.WriteLine("MyStructure.m_iInteger : {0:D}.", MyStructure.m_iInteger);

        // Finally, free the library when it is no longer required.
        FreeLibrary(hDLL);
        hDLL = IntPtr.Zero;
    }
}

The following is a summary of the code above :

  • We used the generic function GetExportedData<T>() to read the data from the DLL with MyStructureTag being the actual parameter for T.
  • Hence at low-level, we used Marshal.PtrToStructure() to read the bytes from the exported unmanaged MyStructure structure and then transform it into its managed counterpart.
  • This is possible only if the managed MyStructureTag has been properly declared using the attributes as shown in point 5.2 above.
  • The managed structure is then updated easily.
  • The updating is done using the generic UpdateExportedData<T>() again with MyStructureTag being the actual parameter for T.
  • Hence at low-level, Marshal.StructureToPtr() is used for updating the DLL exported data.
  • This comes as no surprise as Marshal.StructureToPtr() merely writes to a memory location which it assumes to be fixed in memory.
  • Again, Marshal.StructureToPtr() can only succeed if the managed MyStructureTag has been declared with appropriate use of various attributes.
  • We then used GetExportedData<T>() to re-read the structure from the DLL and we will see that it has indeed ben updated.

6. Writing to Strings in General.

6.1 Before we explore how to access the 3 strings szExportedString, wszExportedString and bstrExported from the DLL, I need to explain carefully the tricky nature of dealing with unmanaged strings.

6.2 Essentially, there are at least 3 types of unmanaged strings that the CLR is equipped to work with :

  • NULL-terminated C-style ANSI string.
  • NULL-terminated C-style Unicode string.
  • BSTR.

6.3 The .NET string is a non-blittable type which means that it cannot be directly transcribed to and from unmanaged memory.

6.4 Furthermore, each of the unmanaged string types of point 6.2 require its own way of translation to and from unmanaged code. There is no generic way to do this.

6.5 A special note must be given to the BSTR. While exported character buffers used for C-style strings can be directly modified in managed code, it is unwise to do so for BSTRs. This will be explained in greater detail later on.

7. Writing to szExportedString – an ANSI C-style String.

7.1 The conversion of an exported ANSI C-style string into a managed string can be done fairly easily. As long as the memory address of the unmanaged string is known, we can use the Marshal.PtrToStringAnsi() method to do the work.

7.2 The conversion of a managed string to an exported unmanaged ANSI string, however, goes deeper than just calling Marshal.StringToHGlobalAnsi() or Marshal.StringToCoTaskMemAnsi().

7.3 This is because the two methods will each first allocate a separate memory buffer and then copy their string contents into this buffer. A pointer to this string buffer will eventually be returned.

7.4 It is not possible to get either of the methods to copy their sting to a pre-prepared buffer.

7.5 Hence the contents of the buffer allocated by either of the methods must be copied to the memory location of the exported string.

7.6 I will demonstrate how this can be done when I discuss writing to exported Unicode strings (next section).

8. Writing to wszExportedString – a Unicode C-style String.

8.1 Just like its ANSI counterpart, the conversion of an exported Unicode C-style string into a managed string can be done fairly easily. The Marshal.PtrToStringUni() method is available for this purpose.

8.2 The down side of things is that : just like the ANSI string, the conversion of a managed string to an exported unmanaged Unicode string is not so straight-forward.

8.3 Either of the Marshal.StringToHGlobalUni() or the Marshal.StringToCoTaskMemUni() methods can be used to convert a managed string into an unmanaged Unicode string. However. the two methods will each allocate a separate unmanaged memory buffer and then copy their string contents into this buffer. A pointer to this unmanaged string buffer will be returned.

8.4 It is not possible to get either of the methods to copy their sting to a pre-prepared buffer. Hence the contents of the buffer allocated by either of the methods must be copied to the memory location of the exported Uicode string.

8.5 I shall demonstrate how this is done through the following C# code that also performs writing to ANSI strings :

private class STRING_TYPE { }
private class ANSI : STRING_TYPE { }
private class UNICODE : STRING_TYPE { }
private class BSTR : STRING_TYPE { }

private static void UpdateExportedDataForStringAnsiOrUnicode<T>(IntPtr pExportedDataAddress, string value, T string_type)
  where T : STRING_TYPE
{
    int iByteSizeOfCharacter = 0;
    IntPtr pUnmanagedString = IntPtr.Zero;

    if (typeof(T) == typeof(ANSI))
    {
        iByteSizeOfCharacter = sizeof(sbyte);
        pUnmanagedString = Marshal.StringToHGlobalAnsi(value);
    }
    else
    {
        iByteSizeOfCharacter = sizeof(char);
        pUnmanagedString = Marshal.StringToHGlobalUni(value);
    }

    // Allocate a byte array with a size according to the character byte size of the
    // current string type. Also allocate space for a terminating NULL character.
    byte[] byArray = new byte[(value.Length * iByteSizeOfCharacter) + iByteSizeOfCharacter];

    // Copy all ANSI characters from pUnmanagedString to the byte array.
    Marshal.Copy(pUnmanagedString, byArray, 0, (value.Length * iByteSizeOfCharacter));
    // Make sure the last byte element is a null value.
    byArray[value.Length] = 0;

    // Now copy all bytes from the byte array to the pExportedDataAddress.
    Marshal.Copy(byArray, 0, pExportedDataAddress, byArray.Length);

    // We must not forget to free the unmanaged memory which contains
    // a copy of the "value".
    Marshal.FreeHGlobal(pUnmanagedString);
    pUnmanagedString = IntPtr.Zero;
}

The following are pertinent points about the code above :

About the STRING_TYPE and Derived Classes.

  • UpdateExportedDataForStringAnsiOrUnicode<T>() is a generic function that is used to write to the character buffers of unmanaged ANSI or Unicode strings.
  • Here, I used the technique of an empty class (or trivial class) to create types that signify other types by being instantiated as generic type parameters. 
  • In UpdateExportedDataForStringAnsiOrUnicode<T>(), T can be instantiated to one of the STRING_TYPE derived classes : ANSI, UNICODE or BSTR.
  • However, notice that inside the function, T is examined for its actual type but the T parameter “string_type” is never used. T serves only to determine whether UpdateExportedDataForStringAnsiOrUnicode<T>() is being used to process an ANSI or a Unicode string.
  • Although T can be instantiated to BSTR, this is not expected because, as its name implies, UpdateExportedDataForStringAnsiOrUnicode<T>() is for writing to exported unmanaged ANSI or Unicode strings. In a later section, I shall demonstrate how to write to a BSTR.

About the UpdateExportedDataForStringAnsiOrUnicode<T>() Function.

  • Because of close similarity bewteen the handling of ANSI and Unicode strings, I have conflated the code for writing to both types of exported unmanaged strings into one function.
  • Central in UpdateExportedDataForStringAnsiOrUnicode<T>() is determining the size of the unmanaged character currently being processed.
  • If T is ANSI, we use the size of a signed byte, sbyte (1 byte). We also use the Marshal.StringToHGlobalAnsi() function to create an unmanaged ANSI string from the “value” managed string (the second parameter).
  • If T is Unicode, we use the size of a managed char which is Unicode (2 bytes). We also use the Marshal.StringToHGlobalUni() function to create an unmanaged Unicode string from “value”.
  • The unmanaged string (ANSI or Unicode) is pointed to by “pUnmanagedString”.
  • As there is no overload of Marshal.Copy() that can be used to copy bytes from one a memory location pointed to by an IntPtr to another, we use a temporary buffer to copy the bytes of the unmanaged string in “pUnmanagedString”.
  • The contents of the bytes buffer is then copied to the target address of the exported DLL string.
  • Finally, the unmanaged string created by either Marshal.StringToHGlobalAnsi() or Marshal.StringToHGlobalUni() must be freed since it is only used temporarily.

8.6 The code below shows how reading and writing to a DLL exported ANSI C-style string can be performed :

private static void TestLoadAndUpdateExportedAnsiStringData()
{
    // Load the DLL.
    IntPtr hDLL = LoadLibrary("TestDLL.dll");

    // Perform action only if we are able to load it.
    if (hDLL != IntPtr.Zero)
    {
        // Obtain the runtime address of the exported data with name "szExportedString".
        IntPtr pszExportedString = (IntPtr)GetProcAddress(hDLL, "szExportedString");
        // Obtain the value of the data.
        string szExportedString = GetExportedDataForString(pszExportedString, new ANSI());
        // Display the data on the console output.
        Console.WriteLine("szExportedString : {0:S}", szExportedString);

        // Change the value of the exported data.
        szExportedString += " Now modified in .NET.";
        UpdateExportedDataForString(pszExportedString, szExportedString, new ANSI());

        // Re-obtain the value of the exported data.
        szExportedString = GetExportedDataForString(pszExportedString, new ANSI());
        // Display the updated data on the console output.
        Console.WriteLine("szExportedString : {0:S}", szExportedString);

        // Finally, free the library when it is no longer required.
        FreeLibrary(hDLL);
        hDLL = IntPtr.Zero;
    }
}

8.7 And the code for reading and writing to a Unicode string :

private static void TestLoadAndUpdateExportedUnicodeStringData()
{
    // Load the DLL.
    IntPtr hDLL = LoadLibrary("TestDLL.dll");

    // Perform action only if we are able to load it.
    if (hDLL != IntPtr.Zero)
    {
        // Obtain the runtime address of the exported data with name "wszExportedString".
        IntPtr pwszExportedString = (IntPtr)GetProcAddress(hDLL, "wszExportedString");
        // Obtain the value of the data.
        string wszExportedString = GetExportedDataForString(pwszExportedString, new UNICODE());
        // Display the data on the console output.
        Console.WriteLine("wszExportedString : {0:S}", wszExportedString);

        // Change the value of the exported data.
        wszExportedString += " Now modified in .NET.";
        UpdateExportedDataForString(pwszExportedString, wszExportedString, new UNICODE());

        // Re-obtain the value of the exported data.
        wszExportedString = GetExportedDataForString(pwszExportedString, new UNICODE());
        // Display the updated data on the console output.
        Console.WriteLine("wszExportedString : {0:S}", wszExportedString);

        // Finally, free the library when it is no longer required.
        FreeLibrary(hDLL);
        hDLL = IntPtr.Zero;
    }
}

8.8 Notice that both TestLoadAndUpdateExportedAnsiStringData() and TestLoadAndUpdateExportedUnicodeStringData() are very similar except for places where the name of the exported data as well as the string type to process are specified.

8.9 Both functions use the generic function GetExportedDataForString<T>() to read the exported string from the DLL. This function is listed below :

private static string GetExportedDataForString<T>(IntPtr pExportedDataAddress, T string_type)
  where T : STRING_TYPE
{
    if (typeof(T) == typeof(ANSI))
    {
        return Marshal.PtrToStringAnsi(pExportedDataAddress);
    }
    else if (typeof(T) == typeof(UNICODE))
    {
        return Marshal.PtrToStringUni(pExportedDataAddress);
    }
    else
    {
        //  T must be BSTR.
        IntPtr[] pIntPtr = new IntPtr[1];

        Marshal.Copy(pExportedDataAddress, pIntPtr, 0, 1);

        if (pIntPtr[0] == IntPtr.Zero)
        {
            return "";
        }
        else
        {
            return Marshal.PtrToStringBSTR(pIntPtr[0]);
        }
    }
}

This function converts the unmanaged string contents at the exported address into an ANSI or Unicode string according to what T is instantiated to. For ANSI and Unicode strings, the conversion is simple : either Marshal.PtrToStringAnsi() or Marshal.PtrToStringUni() is used. I shall explain how the BSTRs are converted in the next section. 

8.10 Both functions also use the UpdateExportedDataForString<T>() generic function to write to the exported data. Here is the listing :

private static void UpdateExportedDataForString<T>(IntPtr pExportedDataAddress, string value, T string_type)
  where T : STRING_TYPE
{
    if ((typeof(T) == typeof(ANSI)) || (typeof(T) == typeof(UNICODE)))
    {
        UpdateExportedDataForStringAnsiOrUnicode(pExportedDataAddress, value, string_type);
    }
    else if (typeof(T) == typeof(BSTR))
    {
        UpdateExportedDataForBSTR(pExportedDataAddress, value, new BSTR());
    }
}

I have expounded on the low-level UpdateExportedDataForStringAnsiOrUnicode<T>() function previously (point 8.5). I shall explain UpdateExportedDataForBSTR<T>() in the next section where we discuss BSTRs.

9. Reading From and Writing To bstrExported – a BSTR.

9.1 An exported BSTR requires special processing as this section will show. Here, I will carefully expound on the reading and writing of an exported BSTR.

9.2 A BSTR is actually a pointer. This fact must be understood well and remembered. It points to a memory address in which the actual string is stored. The string is in Unicode format.

9.3 However, a BSTR string must not be regarded as merely a Unicode string. A BSTR string is always preceded by a 4-byte length indicator.

9.4 A BSTR is best allocated using SysAllocString() and freed using SysFreeString() (see The Importance of Proper BSTR Allocation for more details).

9.5 Manual manipulation of a BSTR’s string contents or its length indicator must be avoided. A BSTR string is best regarded as immutable (once allocated, not to be modified, only freed). Hence, in general, what do we do with a BSTR variable ? We either free the BSTR or we assign the variable another BSTR. We must never modify the BSTR itself once it has been allocated.

On Reading an Exported BSTR Data.

9.6 The conversion of a BSTR into a managed string can be done using Marshal.PtrToStringBSTR(). However, note well that the exported “BSTR” data from TestDLL.dll is not actually a BSTR, but a pointer to a BSTR.

9.7 For a clearer illustration of this, examine the following function which serves as the starting point for reading and writing to an exported BSTR :

private static void TestLoadAndUpdateExportedBSTRData()
{
    // Load the DLL.
    IntPtr hDLL = LoadLibrary("TestDLL.dll");

    // Perform action only if we are able to load it.
    if (hDLL != IntPtr.Zero)
    {
        // Obtain the runtime address of the exported data with name "bstrExported".
        IntPtr pbstrExported = (IntPtr)GetProcAddress(hDLL, "bstrExported");
        // Obtain the value of the data.
        string bstrExported = GetExportedDataForString(pbstrExported, new BSTR());
        // Display the data on the console output.
        Console.WriteLine("bstrExported : {0:S}", bstrExported);

        // Change the value of the exported data.
        bstrExported += " Now modified in .NET.";
        UpdateExportedDataForString(pbstrExported, bstrExported, new BSTR());

        // Re-obtain the value of the exported data.
        bstrExported = GetExportedDataForString(pbstrExported, new BSTR());
        // Display the updated data on the console output.
        Console.WriteLine("bstrExported : {0:S}", bstrExported);

        // Finally, free the library when it is no longer required.
        FreeLibrary(hDLL);
        hDLL = IntPtr.Zero;
    }
}

In the above code, after GetProcAddress() is called, what is returned is actually the entry point to a BSTR named “bstrExported”.

Hence “pbstrExported” actually contains the address where the BSTR “bstrExported” is stored. The BSTR “bstrExported” eventually points to where the actual string is stored. The situation can be summarized by the diagram below :

9.8 Hence when GetExportedDataForString<T>() is called to convert “pbstrExported” into a managed string, the BSTR is actually the contents of the memory location pointed to by “pbstrExported”. “pbstrExported” itself is not the BSTR.

9.9 To do this, please refer to the code of GetExportedDataForString<T>() (point 8.9). The relevant code fragment is displayed below :

private static string GetExportedDataForString(IntPtr pExportedDataAddress, T string_type)
  where T : STRING_TYPE
{
	...
	...
	...

        //  T must be BSTR.
        IntPtr[] pIntPtr = new IntPtr[1];

        Marshal.Copy(pExportedDataAddress, pIntPtr, 0, 1);

        if (pIntPtr[0] == IntPtr.Zero)
        {
            return "";
        }
        else
        {
            return Marshal.PtrToStringBSTR(pIntPtr[0]);
        }
}

We use Marshal.Copy() to copy the contents of the address of the exported BSTR into an IntPtr array of one element. Hence pIntPtr[0] would be equivalent to the “bstrExported” BSTR.

After that, we must check whether pIntPtr[0] is zero. If so, we must not use Marshal.PtrToStringBSTR() to convert it into a managed string. Doing so will result in an exception. Instead, we will return a string with no characters.

If pIntPtr[0] is non-zero, we will then use Marshal.PtrToStringBSTR() to convert it into a managed string and return it.

On Writing to Exported BSTR Data.

9.10 The writing of data to an exported BSTR must use the same logic that we have been discussing so far : the exported data named “bstrExported” is not a BSTR but a pointer to a BSTR.

9.11 In the code of point 9.7, “pbstrExported”, an IntPtr, contains an address that points to the location where the BSTR string resides.

9.12 The low-level code for updating to the BSTR is provided by UpdateExportedDataForBSTR<T>() :

private static void UpdateExportedDataForBSTR<T>(IntPtr pExportedDataAddress, string value, T string_type)
  where T : BSTR
{
    // Note that pExportedDataAddress is expected to be the container
    // of a BSTR (remember that a BSTR is a 4-byte pointer).
    // The goal of this function is to change the contents of
    // pExportedDataAddress so that it points to a different BSTR.

    // Allocate a fresh new BSTR in memory somewhere.
    // using a single-element IntPtr array as the container
    // of the BSTR.
    IntPtr[] pBSTR = new IntPtr[1];

    // Convert "value", a managed string, into a BSTR
    // and store this BSTR in pBSTR[0].
    pBSTR[0] = Marshal.StringToBSTR(value);

    // Copy the BSTR (a pointer value) to pExportedDataAddress.
    Marshal.Copy(pBSTR, 0, pExportedDataAddress, 1);
}

Here is where careful explanation must be given to the logic of what we are trying to accomplish :

  • As mentioned in point 9.5, once a BSTR varianle has been assigned a value, we must either use it to free the actual string of the BSTR or we assign the variable another BSTR allocated somewhere else.
  • This is what UpdateExportedDataForBSTR<T>() does.
  • Recall in TestLoadAndUpdateExportedBSTRData() (point 9.7) that when we use GetProcAddress() to access the exported data named “bstrExported”, what is returned is actually a pointer to a BSTR global variable in TestDLL.dll.
  • Hence “pbstrExported” points to the variable named “bstrExported” in the DLL.
  • What we must do is not to change the BSTR that is held by “bstrExported” but to make “bstrExported” point to another BSTR.
  • This is the meaning of modifying a BSTR variable which happens also to be exported.

9.13 Note well that because UpdateExportedDataForBSTR<T>() will ultimately change the “bstrExported” BSTR, the original BSTR will no longer be reacheable unless it is held in another variable. The example given here is for illustrative purposes only. In actual production code, this fact must be carefully noted to avoid memory leakage.

10. In Conclusion.

10.1 Although this article deals with reading and writing exported DLL data, the principles and code presented can be applied generically to the process of serializing/deserializing managed data to and from any fixed memory locations where unmanaged data reside.

10.2 I hope the reader will find the occassion to use the generic functions presented here.

 

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: