I'm encountering a memory leak while using an activeX component in my project. I'm working with Embarcadero Rad Studio 10.2 and developing a C++ industrial program that needs to communicate with a Codesys soft PLC on the same machine.
So, i have a ActiveX component that can handle the communication part between my program and the soft PLC.
I imported the ActiveX and everything seemed OK but i've found a memory leak that fills about 20MB per hour... To import the library i followed the official guide: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Registering_a_COM_Object
I made lot of tests and I realized that the memory leak happens every time I work with ActiveX methods with variants involved. Looks like the program is not able to free some kind of temporary variants used by the component.
I've tested Visual Studio examples and everything works fine, so i think that the problems is generated by the type library that Rad Studio generates when I import the activeX component. Also the ActiveX developer claims that everything works with Visual Studio.
I used also Dr. Memory and other tools that confirm the presence of the leak but can't provide details because i think the ActiveX is not compiled for debug.
Any idea about the reason for a behaviour like that?
There is some possibile incompatibility for an ActiveX in RAD studio?
Thanks in advance
Edit
An example that shows the ActiveX usage.
Unit1.cpp
#include <vcl.h>
#pragma hdrstop
#pragma package(smart_init)
#pragma resource "*.dfm"
#include <System.IOUtils.hpp>
#include "Unit1.h"
TForm1 *Form1;
// ---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
counter = 0;
// Setting the path for communication setting files required for later connection
if (TFile::Exists("PLCHandler.ini"))
{
iniPath = (wchar_t*)L"PLCHandler.ini";
logPath = (wchar_t*)L"Log.txt";
}
iResult = PLCHandler->MCreate(&iHandle);
try
{
// Creating the component and retrieving the handle for other methods
iResult = PLCHandler->MCreate(&iHandle);
if (iResult == 0)
{
iResult = PLCHandler->MConnect(iHandle, 0, iniPath, logPath);
if (iResult == 0)
{
connected = true;
LabeledEdit1->Text = "CONNECTED";
long int numeroSimboli = 0;
PLCHandler->MGetNumberOfSymbols(iHandle, &numeroSimboli);
LabeledEdit2->Text = numeroSimboli;
PLCHandler->MGetPlcStatus(iHandle, &iPLCStatus);
LabeledEdit3->Text = iPLCStatus;
}
}
else
{
LabeledEdit2->Text = "ERROR: " + (String)iResult;
}
}
catch (...)
{
LabeledEdit2->Text = "ERROR";
}
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
// Timers for testing purposes, they launch the next method every ms. Changing timing only delays the problem
Timer1->Enabled = !Timer1->Enabled;
Timer2->Enabled = !Timer2->Enabled;
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
// Asking to the PLC Handler the value of a PLC variable, identified by name
Variant varReceived;
BSTR name = SysAllocString(L"Test.GVL.Test_INT");
try
{
counter++;
LabeledEdit1->Text = counter;
// This is where i suppose the memory leak happens; the problem vanishes commenting the next line
varReceived = PLCHandler->MSyncReadVarFromPlc(iHandle, &iResult, name, 2);
LabeledEdit3->Text = varReceived.GetElement(0);
SysFreeString(name);
VarClear(varReceived);
}
catch (...)
{
VarClear(varReceived);
SysFreeString(name);
}
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
Timer1Timer(this);
}
// ---------------------------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
// Other test: destroy the component and recreates it: the memory usages remains the same, no deallocation happens
try
{
PLCHandler->MDelete(&iHandle);
iResult = PLCHandler->MCreate(&iHandle);
if (iResult == 0)
{
iResult = PLCHandler->MConnect(iHandle, 0, iniPath, logPath);
if (iResult == 0)
{
connected = true;
LabeledEdit1->Text = "CONNECTED";
long int numeroSimboli = 0;
PLCHandler->MGetNumberOfSymbols(iHandle, &numeroSimboli);
LabeledEdit2->Text = numeroSimboli;
PLCHandler->MGetPlcStatus(iHandle, &iPLCStatus);
LabeledEdit3->Text = iPLCStatus;
}
}
else
{
LabeledEdit2->Text = "ERROR: " + (String)iResult;
}
}
catch (...)
{
LabeledEdit2->Text = "ERROR";
}
}
// ---------------------------------------------------------------------------
Unit1.h
#ifndef Unit1H
#define Unit1H
// ---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Vcl.ExtCtrls.hpp>
#include <Vcl.OleCtrls.hpp>
#include "PLCHANDLERXLib_OCX.h"
// ---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published: // IDE-managed Components
TTimer * Timer1;
TButton * Button1;
TLabeledEdit *LabeledEdit1;
TTimer * Timer2;
TLabeledEdit *LabeledEdit2;
TButton * Button3;
TPLCHandlerX *PLCHandler;
TLabeledEdit *LabeledEdit3;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
void __fastcall Button3Click(TObject *Sender);
private:
// User declarations
public: // User declarations
long int counter;
wchar_t* iniPath;
wchar_t* logPath;
long int iPLCStatus;
long int iHandle;
long int readSize;
long int writeSize;
long int iResult;
Byte unbyte;
bool connected;
__fastcall TForm1(TComponent* Owner);
};
// ---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
// ---------------------------------------------------------------------------
#endif
And as requested the TLB generated by RAD Studio while importing the ActiveX
.cpp File
// ************************************************************************ //
// WARNING
// -------
// The types declared in this file were generated from data read from a
// Type Library. If this type library is explicitly or indirectly (via
// another type library referring to this type library) re-imported, or the
// 'Refresh' command of the Type Library Editor activated while editing the
// Type Library, the contents of this file will be regenerated and all
// manual modifications will be lost.
// ************************************************************************ //
// $Rev: 87174 $
// File generated on 14/03/2018 11:22:13 from Type Library described below.
// ************************************************************************ //
// Type Lib: C:\PLCHandler_SDK_Windows_v16\bin\Windows\PLCHandlerX.ocx (1)
// LIBID: {BB4C0C2B-D94B-4F5C-A774-4DF59A2227FF}
// LCID: 0
// Helpfile: C:\PLCHandler_SDK_Windows_v16\bin\Windows\PLCHandlerX.hlp
// HelpString: PLCHandlerX ActiveX Control module
// DepndLst:
// (1) v2.0 stdole, (C:\Windows\SysWOW64\stdole2.tlb)
// SYS_KIND: SYS_WIN32
// ************************************************************************ //
#include <vcl.h>
#pragma hdrstop
#include "PLCHANDLERXLib_TLB.h"
#if !defined(__PRAGMA_PACKAGE_SMART_INIT)
#define __PRAGMA_PACKAGE_SMART_INIT
#pragma package(smart_init)
#endif
namespace Plchandlerxlib_tlb
{
// *********************************************************************//
// GUIDS declared in the TypeLibrary
// *********************************************************************//
const GUID LIBID_PLCHANDLERXLib = {0xBB4C0C2B, 0xD94B, 0x4F5C,{ 0xA7, 0x74, 0x4D,0xF5, 0x9A, 0x22,0x27, 0xFF} };
const GUID DIID__DPLCHandlerX = {0xA51B6208, 0x4C76, 0x4E79,{ 0xAC, 0x93, 0xB4,0x15, 0x7D, 0x6D,0x97, 0xC5} };
const GUID DIID__DPLCHandlerXEvents = {0xF2CC045D, 0x93E1, 0x4FE1,{ 0xA1, 0x5F, 0xE6,0x48, 0x18, 0x85,0x35, 0x5A} };
const GUID CLSID_PLCHandlerX = {0x99036BDD, 0x9A94, 0x4ED2,{ 0x89, 0x61, 0x42,0x0C, 0x74, 0xDD,0x51, 0xCE} };
};
.h File is too long for the question body (Full code here), but the MSyncReadVarsFromPlc method is
VARIANT __fastcall MSyncReadVarsFromPlc(long lHandle, long* plResult, BSTR pszSymbols, VARIANT SizeList, long lNumOfVars)
{
_TDispID _dispid(/* MSyncReadVarsFromPlc */ DISPID(45));
TAutoArgs<5> _args;
_args[1] = lHandle /*[VT_I4:0]*/;
_args[2] = plResult /*[VT_I4:1]*/;
_args[3] = pszSymbols /*[VT_BSTR:0]*/;
_args[4] = SizeList /*[VT_VARIANT:0]*/;
_args[5] = lNumOfVars /*[VT_I4:0]*/;
OleFunction(_dispid, _args);
return _args.GetRetVariant();
}
As you can see in the TLB, the MSyncReadVars method returns a VARIANT that actually contains an array of bytes with the requested variables values.
The Variant varReceived stores the returned VARIANT but is deallocated with VarClear when finished.
Any idea of what can generate the memory leak?
My feeling is that the returned VARIANT from MSyncReadVarsFromPlc is not deallocated after the method execution. But I can't see any way to solve this issue, also because same usage in a Visual Studio example works fine.
Can an ActiveX work fine in Visual Studio and not in RAD Studio?
You have a memory leak when calling MSyncReadVarFromPlc()
. It returns an OLE VARIANT
, which you are assigning to an RTL Variant
. That assignment copies the data, and then the leak occurs because you are not calling VariantClear()
on the original VARIANT
.
A VARIANT
is just a struct with data fields. Assigning a VARIANT
directly to another VARIANT
without using VariantCopy()
just copies the field values as-is. Dynamically allocated data, like strings and arrays, are not re-allocated, the pointers are copied as-is.
A Variant
, on the other hand, is a class wrapper that has copy semantics. Assigning a VARIANT
(or another Variant
) to a Variant
allocates a new copy of dynamic data, preserving the original. The original and copy need to be cleared separately. You are leaking because you are not clearing the original VARIANT
, only the copied Variant
.
Change your call to MSyncReadVarFromPlc()
to save the return value to a VARIANT
instead of a Variant
, and then call VariantClear()
when you are done using it:
VARIANT varReceived;
...
varReceived = PLCHandler->MSyncReadVarFromPlc(iHandle, &iResult, name, 2);
...
VariantClear(&varReceived);