CWindow RC19

Started by José Roca, September 01, 2016, 01:44:13 AM

Previous topic - Next topic

José Roca

CWindow Release Candidate 19

Incorporates all the changes discussed in the previous thread.

The Variant class has been renamed from CVar to CVariant.

New class, CSafeArray, to work with safe arrays of any size and dimensions, and of any type supported by safe array Windows API.

Updated help file.

José Roca

Example of a one-dimensional VARIANT safe array.


' ========================================================================================
' CSafeArray test
' ========================================================================================

#include "Afx/CSafeArray.inc"
using Afx

' // One-dimensional array of VT_VARIANT
' // Two elements, lower-bound 1
DIM csa AS CSafeArray = CSafeArray(VT_VARIANT, 2, 1)
DIM cv1 AS CVARIANT = "Test variant 1"
csa.PutElement(1, @cv1)
DIM cvOut AS CVARIANT
csa.GetElement(1, @cvOut)
print cvOut

DIM cv2 AS CVARIANT = "Test variant 2"
csa.PutElement(1, @cv2)
csa.GetElement(1, @cvOut)
print cvOut

' // Redim (preserve) the safe array
csa.Redim(1, 3)
DIM cv3 AS CVARIANT = "Test variant 3"
csa.PutElement(3, @cv3)
csa.GetElement(3, @cvOut)
print cvOut

PRINT
PRINT "Press any key..."
SLEEP


José Roca

Example of a two-dimensional safe array.


' ========================================================================================
' CSafeArray test
' ========================================================================================

#include "Afx/CSafeArray.inc"
using Afx

' // Two-dimensional array of BSTR
' // 1D: elements = 5, lower bound = 1
' // 1D: elements = 3, lower bound = 1
'DIM rgsabounds(0 TO 1) AS SAFEARRAYBOUND = {(5, 1), (3, 1)}
'DIM csa AS CSafeArray = CSafeArray(VT_BSTR, 2, @rgsabounds(0))
DIM csa AS CSafeArray = CSafeArray(VT_BSTR, 5, 1, 3, 1)

' // array index: first element, first dimension
'DIM rgidx(0 TO 1) AS LONG = {1, 1}
' // Put the value
DIM cbs1 AS CBSTR = "Test string 1.1"
'csa.PutElement(@rgidx(0), *cbs1)
csa.PutElement(1, 1, *cbs1)

' // array index: second element, first dimension
'rgidx(0) = 2 : rgidx(1) = 1
' // Put the value
DIM cbs2 AS CBSTR = "Test string 2.1"
'csa.PutElement(@rgidx(0), *cbs2)
csa.PutElement(2, 1, *cbs2)

' // array index: first element, second dimension
'rgidx(0) = 1 : rgidx(1) = 2
' // Put the value
DIM cbs3 AS CBSTR = "Test string 1.2"
'csa.PutElement(@rgidx(0), *cbs3)
csa.PutElement(1, 2, *cbs3)

' // array index: second element, second dimension
'rgidx(0) = 2 : rgidx(1) = 2
' // Put the value
DIM cbs4 AS CBSTR = "Test string 2.2"
'csa.PutElement(@rgidx(0), *cbs4)
csa.PutElement(2, 2, *cbs4)

DIM hr AS HRESULT, cbsOut AS CBSTR
hr = csa.GetElement(1, 1, @cbsOut)
print cbsOut
hr = csa.GetElement(2, 1, @cbsOut)
print cbsOut
print

hr = csa.GetElement(1, 2, @cbsOut)
print cbsOut
hr = csa.GetElement(2, 2, @cbsOut)
print cbsOut

PRINT
PRINT "Press any key..."
SLEEP
[code]

José Roca

#3
For more dimensions, you have to fill a SAFEARRAYBOUND structure, e.g.


DIM rgsabounds(0 TO 2) AS SAFEARRAYBOUND = {(5, 1), (3, 1), (10, 0)}
DIM csa AS CSafeArray = CSafeArray(VT_BSTR, 3, @rgsabounds(0))


This creates a three-dimensional safe array with 5 elements and a lower bound of 1 for the first dimension, 3 elements and a lower bound of 1 for the second dimension, and 10 elements and a lower bound of 0 for the third dimension.

Like Variants, safe arrays are used in COM Automation programming for its flexibility and to provide a standard to pass data between different languages, but aren't intended for general purpose programming (except in VB6) because they are slower than the ones supported by the various compilers. Usually, every compiler implements its own flavour of arrays, thet are incompatible with other languages. You can pass a pointer to the data, but you can't pass the array descriptor. Safe arrays carry an array descriptor with them, allowing to access all kind of information, such the number of dimensions and the elements, the lower bounds, the data type...

The Windows API for safe arrays, sometimes uses the pair elements / lower boud, and sometimes the pair lower bound / elements. For consistency, the methods of my class always use the pair elements / lower bound.


José Roca

Modified the following methods


' ========================================================================================
' Returns a variant containing a copy of the safe array.
' ========================================================================================
PRIVATE FUNCTION CSafeArray.CopyToVariant () AS VARIANT
   CSAFEARRAY_DP("CSafeArray CopyToVariant")
   DIM v AS VARIANT, vt AS VARTYPE
   IF m_psa = NULL THEN RETURN v
   SafeArrayGetVartype(m_psa, @vt)
   v.vt = vt OR VT_ARRAY
   SafeArrayCopy(m_psa, @v.parray)
   RETURN v
END FUNCTION
' ========================================================================================

' ========================================================================================
' Transfers ownership of the safe array to a variant and detaches it from the object.
' ========================================================================================
PRIVATE FUNCTION CSafeArray.MoveToVariant () AS VARIANT
   CSAFEARRAY_DP("CSafeArray MoveToVariant")
   DIM v AS VARIANT, vt AS VARTYPE
   SafeArrayGetVartype(m_psa, @vt)
   IF m_psa = NULL THEN RETURN v
   v.vt = vt OR VT_ARRAY
   SafeArrayUnlock(m_psa)
   v.parray = m_psa
   m_psa = NULL
   RETURN v
END FUNCTION
' ========================================================================================


In both I was assigning just the var type


v.vt = vt


instead of


v.vt = vt OR VT_ARRAY


New file uploaded.

José Roca

Discovered it while writing this test:


' ========================================================================================
' CSafeArray test
' ========================================================================================

#include "Afx/CSafeArray.inc"
using Afx

DIM csv AS CSafeArray
csv.CreateVector(VT_VARIANT, 3, 1)
DIM cv1 AS CVARIANT = "String 1"
csv.PutElement(1, @cv1)
DIm cvOut AS CVARIANT
csv.GetElement(1, @cvOut)
print cvOut
DIM cv2 AS CVARIANT = "String 2"
csv.PutElement(2, @cv2)
csv.GetElement(2, @cvOut)
print cvOut
DIM cv3 AS CVARIANT = "String 3"
csv.PutElement(3, @cv3)
csv.GetElement(3, @cvOut)
print cvOut

DIM cv AS CVARIANT
cv = csv.MoveToVariant
print HEX(cv.vd.vt)
print cv

PRINT
PRINT "Press any key..."
SLEEP


José Roca

There is still a problem with vectors. The code changed should be


IF (m_psa->fFeatures AND FADF_FIXEDSIZE) = FADF_FIXEDSIZE THEN
   v.vt = vt OR VT_VECTOR
ELSE
   v.vt = vt OR VT_ARRAY
END IF


But when I check for the FADF_FIXEDSIZE it always returns FALSE. It should return TRUE if it is a vector.

José Roca

It is madening. The MSDN documentation for SafeArrayCreateVector says that "a safe array created with SafeArrayCreateVector is a fixed size, so the constant FADF_FIXEDSIZE is always set," yet if I create one this flag is not set.

José Roca

#8
Changed the following functions. The old ones could cause a memory leak if assigned to a CVariant instead of a Variant.


' ========================================================================================
' Copies the contents of the safe array to a variant.
' ========================================================================================
PRIVATE FUNCTION CSafeArray.CopyToVariant (BYVAL pvar AS VARIANT PTR) AS HRESULT
   CSAFEARRAY_DP("CSafeArray CopyToVariant")
   IF m_psa = NULL THEN RETURN E_FAIL
   IF pvar = NULL THEN RETURN E_INVALIDARG
   VariantClear(pvar)
   DIM vt AS VARTYPE
   SafeArrayGetVartype(m_psa, @vt)
   IF (m_psa->fFeatures AND FADF_FIXEDSIZE) = FADF_FIXEDSIZE THEN
      pvar->vt = vt OR VT_VECTOR
   ELSE
      pvar->vt = vt OR VT_ARRAY
   END IF
   RETURN SafeArrayCopy(m_psa, @pvar->parray)
END FUNCTION
' ========================================================================================

' ========================================================================================
' Transfers ownership of the safe array to a variant and detaches it from the object.
' ========================================================================================
PRIVATE FUNCTION CSafeArray.MoveToVariant (BYVAL pvar AS VARIANT PTR) AS HRESULT
   CSAFEARRAY_DP("CSafeArray MoveToVariant")
   IF m_psa = NULL THEN RETURN E_FAIL
   IF pvar = NULL THEN RETURN E_INVALIDARG
   VariantClear(pvar)
   DIM vt AS VARTYPE
   SafeArrayGetVartype(m_psa, @vt)
   IF (m_psa->fFeatures AND FADF_FIXEDSIZE) = FADF_FIXEDSIZE THEN
      pvar->vt = vt OR VT_VECTOR
   ELSE
      pvar->vt = vt OR VT_ARRAY
   END IF
   FUNCTION = SafeArrayUnlock(m_psa)
   pvar->parray = m_psa
   m_psa = NULL
END FUNCTION
' ========================================================================================


José Roca

Same for CVariant.Detach.


' ========================================================================================
' Detaches the variant data from this class and returs it as a VARIANT.
' Don't clear vd with VariantClear because we are transfering ownership.
' ========================================================================================
PRIVATE FUNCTION CVariant.Detach (BYVAL pvar AS VARIANT PTR) AS HRESULT
   CVARIANT_DP("CVARIANT DETACH")
   IF pvar = NULL THEN RETURN E_INVALIDARG
   VariantClear(pvar)
   DIM pdest AS ANY PTR = memcpy(pvar, @vd, SIZEOF(VARIANT))
   IF pdest = NULL THEN RETURN E_FAIL
   vd.vt = VT_EMPTY
   RETURN S_OK
END FUNCTION
' ========================================================================================


José Roca

And same for...


' ========================================================================================
' Returns a variant containing a copy of the safe array.
' ========================================================================================
PRIVATE FUNCTION CBSTRSA.CopyToVariant (BYVAL pvar AS VARIANT PTR) AS HRESULT
   CBSTRSA_DP("CBSTRSA CopyToVariant")
   IF m_psa = NULL THEN RETURN E_FAIL
   IF pvar = NULL THEN RETURN E_INVALIDARG
   VariantClear(pvar)
   pvar->vt = VT_ARRAY OR VT_BSTR
   RETURN SafeArrayCopy(m_psa, @pvar->parray)
END FUNCTION
' ========================================================================================

' ========================================================================================
' Transfers ownership of the safe array to a variant and detaches it from the object.
' ========================================================================================
PRIVATE FUNCTION CBSTRSA.MoveToVariant (BYVAL pvar AS VARIANT PTR) AS HRESULT
   CBSTRSA_DP("CBSTRSA MoveToVariant")
   IF m_psa = NULL THEN RETURN E_FAIL
   IF pvar = NULL THEN RETURN E_INVALIDARG
   VariantClear(pvar)
   FUNCTION = SafeArrayUnlock(m_psa)
   pvar->vt = VT_ARRAY OR VT_BSTR
   pvar->parray = m_psa
   m_psa = NULL
END FUNCTION
' ========================================================================================


José Roca

#11
I will have to rewrite all the code that uses CBSTR strings to get out parameters passing the address of the underlying BSTR with the @ operator. The reason is that the called functions don't free the passed BSTR, but allocate a new one and assign the new pointer to it, causing a memory leak.

For example,

This function leaks memory:


PRIVATE FUNCTION CFileSys.GetFileShortPath (BYREF cbsFile AS CBSTR) AS CBSTR
   DIM cbsPath AS CBSTR
   IF m_pFileSys = NULL THEN RETURN cbsPath
   ' // Get a reference to the IFile interface
   DIM pFile AS Afx_IFile PTR
   SetResult(m_pFileSys->GetFile(cbsFile, @pFile))
   IF pFile THEN
      SetResult(pFile->get_ShortPath(@cbsPath))
      pFile->Release
   END IF
   RETURN cbsPath
END FUNCTION


and this one don't


PRIVATE FUNCTION CFileSys.GetFileShortPath (BYVAL pwszFile AS WSTRING PTR) AS CBSTR
   DIM bstrPath AS AFX_BSTR
   IF m_pFileSys = NULL THEN RETURN NULL
   ' // Get a reference to the IFile interface
   DIM pFile AS Afx_IFile PTR
   SetResult(m_pFileSys->GetFile(pwszFile, @pFile))
   IF pFile THEN
      SetResult(pFile->get_ShortPath(@bstrPath))
      pFile->Release
   END IF
   RETURN bstrPath
END FUNCTION


When returning bstrPath in the second function, the called constructor to return a CBSTR simply attaches the bstrPath pointer, that will be freed when the CBSTR class will be destroyed.

There is no problem when using @ to pass the pointer of the BSTR to an IN parameter, only for OUT parameters.

CWSTR strings look safe, since the API functions that use them overwrite the buffer pointed by the variable instead of allocating a new one and assigning a new pointer to it. They can't do it because the memory is fixed, not dynamic.

The problem is that when using CBSTR with OUT parameters, we will have to use an intermediate BSTR variable, if I don't find another solution.

I will have to investigate about variants too, although apparently they use VariantCopy, because they can't simply assign a new pointer to it without first freeing the memory of the destination variant (it will cause a memory leak if the variant has pointers to BSTRings, interfaces, etc.), and VariantCopy first clears the destination variant with VariantClear.

If BSTRrings wer natively implemented in the language, the compiler could detect if the parameter is an OUT one and free the BSTR before passing it to the function, or something like that. I already I'm using an hack for the constructor and the LET operator, but can't use @ for two different purposes.

José Roca

#12
The MFC CCOmBstr class has the same problem:

Passing the address of an initialized CComBSTR to a function as an [out] parameter causes a memory leak.

In the example below, the string allocated to hold the string "Initialized" is leaked when the function MyGoodFunction replaces the string.
C++

CComBSTR bstrLeak(L"Initialized");
HRESULT hr = MyGoodFunction(&bstrLeak);   

To avoid the leak, call the Empty method on existing CComBSTR objects before passing the address as an [out] parameter.

Note that the same code would not cause a leak if the function's parameter was [in, out].

See: https://msdn.microsoft.com/en-us/library/bdyd6xz6.aspx#programmingwithccombstr_memoryleaks

My first step has been to remove allocation in the default constructor:


CONSTRUCTOR CBStr
'   m_bstr = SysAllocString("")   ' // warning: don't initialize it
   CWSTR_DP("CBSTR CONSTRUCTOR Default - " & WSTR(m_bstr))
END CONSTRUCTOR


I'm going to implement and Empty method, and Attach and Detach methods.

The class itself doesn't need more changes. What needs to be changed is the way in which I have been using it. In the wrapper classes, I will rewrite the code.

Can't use the LET operator to free the BSTR and set it to null if an empty string is passed because an empty string is valid and, of course, is not the same that a null pointer.

José Roca

I have added the following remarks in the help file topic for CBSTR / CWSTR.

Conversion issues

Although several CBSTR /CWSTR methods will automatically convert an ANSI string argument into Unicode, the methods will always return Unicode format strings.

Scope issues

CBSTR will free its resources when it goes out of scope. If a function returns a pointer to the CBSTR string, this can cause problems, as the pointer will reference memory that has already been freed. In these cases, use the Copy method.

Explicitly freeing the CBSTR object

It is possible to explicitly free the string contained in the CBSTR object before the object goes out scope. If the string is freed, the CBSTR object is invalid.


' // Declare a CComBSTR object
DIM bstrMyString AS CBSTR = "Hello World"
' // Free the string explicitly
SysFreeString(bstrMyString)
' // The string will be freed a second time when the CComBSTR object goes out of scope, which is invalid.


Using CBSTR objects in loops

As the CBSTR class allocates and frees new BSTRs to perform certain operations, such as the += operator or Append method, it is not recommended that you perform string manipulation inside a tight loop. In these situations, CWSTR provides better performance.

Memory leak issues

Passing the address of an initialized CBSTR to a function as an [out] parameter causes a memory leak.

In the example below, the string allocated to hold the string "Initialized" is leaked when the function MyFunction replaces the string.


DIM bstrLeak AS CBSTR = "Initialized"
DIM hr AS HRESULT = MyFunction(@bstrLeak)


To avoid the leak, call the Empty method on existing CBSTR objects before passing the address as an [out] parameter.

Note that the same code would not cause a leak if the function's parameter was [in, out].

José Roca

#14
I have checked all the code and oly three of the COM wrapper classes, CFileSys, CDicObj, CTextStream and CWinHttpRequest will need some changes. These are the ones that use CBSTR and or CVARIANT.

The CWSTR and CVARIANT classes are safe. What was unsafe was using the operator @ with OUT parameters. We must pass a newly declared variable or call the Clear method before reusing them. Please note that calling Clear is not the same that setting the variable to "". In the case of a CBSTR, setting it to "" will create an empty BSTR, and in the case of a VARIANT, a VT_BSTR storing an empty BSTR.

Other that this little inconvenience, all seems to be OK.

BTW, the CBSTRSA and CBSTRDIC classes have been deprecated, since now we have CDicObj and CSafeArray, that work with any data type.