Adventures in interop code: explore string interop memory

 

In Create an ActiveX control using ATL that you can use from Fox, Excel, VB6, VB.Net, I showed how to create a control (with which the user can interact) which can be hosted in many places.

 

Today’s sample creates a class in C++ that doesn’t necessarily have UI, and thus isn’t visually hosted, but can still be used from these clients.

 

It allows you to experiment with various coding techniques from within a VB (or C#) app.

 

I was playing around with BSTR and CComBSTR objects and had in the back of my mind that there’s a cache that can get in the way of inspecting shared memory use.

I was using IMallocSpy to track memory use and I knew I had to turn off that cache with SET OANOCACHE=1

 

Sometimes code in one module needs to allocate memory that another module needs to free. For example, across module, thread, process or machine boundaries, strings or structs can be passed by reference: my code may request a string from some other code. That other code needs to allocate the memory, but has no idea when to free it. It is my code’s responsibility to free it. See Memory Management Rules

                                                                                                                                                                                                                                                        

So to experiment, I first created a simple VB Interop sample, then added code to test memory allocations. Then I tried with and without OANOCACHE.

 

To set this option, the environment variable OANOCACHE must be set to 1 before the process starts. One way to do this is to open a CMD prompt, set it, then start the process from that command prompt.

Otherwise, from Control Panel->System->AdvancedSystemSettings->Advanced. (Double Advance means it’s super advanced! J)

 

Try running this code, with and without this option.

 

At first glance, I had originally thought that the way the Ole Automation cache worked was like a string lookup: if the string already exists in the cache, it would be ref counted.

Running this experiment proves otherwise.

 

Also see Raymond’s recent post: https://blogs.msdn.com/oldnewthing/archive/2009/11/27/9929238.aspx

 

 

 

Start Visual Studio (I was using VS 2008 for this sample)

 

File->New->Project->VB WPF Application. I called mine VBInterop.

 

Switch to Window1.xaml.vb, replace the contents with the VB code below.

 

Choose File->Add->New Project->VC++ ATL->ATL Project. I named mine AtlInterop. (Yes: you can add multiple projects to your solution.) The ATL wizard comes up. Just choose the defaults.

 

Choose Project->Add Class ->ATL->ATL Simple Object. I named mine TestInterop. Note how the ATL Simple Object Wizard creates names for the various parts, like “ITestInterop” for the interface, “CTestInterop” for the class, etc.

 

Now choose View->Class View. Navigate to the ATLTestInterop->ITestInterop interface, right click->Add Method to bring up the “Add Method Wizard”

 

Give it a name “Test” with a single “in” parameter of type BSTR, named Parm1 (don’t forget to click on the “Add” button to add the parameter).

Also, add a single Out, Retval parameter of type LONG *, with a name pRetval.

 

The wizard changes a bunch of things, and is unfortunately not reentrant. If you forget to add the retval parameter, this is what it does: changes the IDL (Interface Definition Language), then make the corresponding change to the implementation of that interface (the TestIntop.h and .cpp files).

 

For adding a return value to the Test method. (All COM methods return an HRESULT, which is just an integer. To handle return values, an additional parameter is added and it’s passed ByRef).

 

I opened the AtlInterop.idl file, found the definition of the Test method

            [id(1), helpstring("method Test")] HRESULT Test([in] BSTR str);

 

and changed it to:

            [id(1), helpstring("method Test")] HRESULT Test([in] BSTR str, [out, retval] long *pRetval);

 

 

Go to the Solution Explorer, then open TestInterop.cpp, paste in the below VC code.

 

Build the solution, which builds TestInterop.dll . This can now be used to add a reference to the VB project. You get an error that AtlInteropLib doesn’t exist. Hit F8 (or dbl-click the error) to get to the VB code, then add a COM reference to ATLInterop 1.0 Type Library in the VB project.

 

Hit F5 to go. You can put a breakpoint on the btnClick method, but to break in your C++ code, you need to Project->Properties->Debug->Enable Unmanaged Code debugging.

 

Also, if you’re on a 64 bit OS, you may need to change Project->Compile->Advanced->Target CPU from AnyCPU to x86 to enable interop debugging.

An alternative: you can do all native debugging: set the StartupProject to be AtlInterop by right click on the AtlInterop Project in the Solution Explroer, then from the same menu change the Project->Property->Configuration->Debugging->Command line to point to the VBInterop EXE, set debugger type to “Native”, hit F5

When running, open Task manager and watch how much memory gets used when the loop runs.

Now comment out the SysFreeString line, F5 and observe.

Now try with OANOCACHE=1

See also:

Create an ActiveX control using ATL that you can use from Fox, Excel, VB6, VB.Net

How fast is interop code?

The OLE Memory Allocator: https://msdn.microsoft.com/en-us/library/ms688453(VS.85).aspx

<VB code>

Class Window1

    Private WithEvents m_btn As New Button With {.Content = "Push me"}

    Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded

        Me.Content = m_btn

    End Sub

    Sub btnClick() Handles m_btn.Click

        Dim a = New AtlInteropLib.TestInterop

        a.Test("From VB")

        'from managed code, you can use IMalloc to allocate shared memory

        ' For fun, look at all the other members of Marshal

        Dim mem = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(1000)

        System.Runtime.InteropServices.Marshal.FreeCoTaskMem(mem)

    End Sub

End Class

</VB code>

<VC code>

// TestInterop.cpp : Implementation of CTestInterop

#include "stdafx.h"

#include "TestInterop.h"

// CTestInterop

STDMETHODIMP CTestInterop::Test(BSTR str, LONG* pRetval)

{

      // TODO: Add your implementation code here

      for (int i = 0 ; i < 10000 ; i++)

      {

            char buf[1000];

            CComBSTR str("\nIter # ");

            str.Append(_itoa(i,buf,10));

            OutputDebugString(str);

            for (int j=0 ; j<100000 ; j++)

            {

                  BSTR p = SysAllocString(L"Foobar");

                  if (!p)

                  {

                        ::MessageBox(0,str,L"Out of memory",0);

                        return E_OUTOFMEMORY;

                  }

                  SysFreeString(p);

            }

      }

      ::MessageBox(0,str,L"From VC!",0);

      return S_OK;

}

</VC code>

Comments

  • Anonymous
    January 29, 2010
    Hi, VC programmer passing through here. (Yes, I do like to read other blogs.) I just want to point out a little issue with the C++ code. char buf[1000]; CComBSTR str("nIter # "); str.Append(_itoa(i,buf,10)); You are using ANSI strings here for the CComBSTR. While this is possible it isn't going to be anywhere near as efficient as using wide characters since they will have to go through a seperate call to MultibyteToWideChar. There is a note about this on the CComBSTR class documentation in the MSDN (http://msdn.microsoft.com/en-us/library/zh7x9w3f.aspx). So the following would be a better idea. wchar_t buf[1000]; CComBSTR str(L"nIter # "); str.Append(_itow(i, buf, 10)); Since both the constructor and Append calls wouldn't need to be converted first.