JAL Computing

C++COMProgramming .NET Mac Palm CPP/CLI Hobbies

 

Home
Up

 

Using and Returning SafeArrays

Nothing seems to cause more confusion than the concept of SafeArrays. First, documentation is very sparse. Second, SafeArrays can contain pointers to objects on the system heap (BSTRs) making resource allocation/deallocation critical. You can look at a SafeArray as a Visual Basic Array, which contains information about the array. Alternatively, you can look at a SafeArray as a wrapper class providing high level protected access to an array. In the context of ASP COM programming, the SafeArray is a way to package data for consumption by an ASP script. It is also useful within a COM component that must pass data to an ASP Response object. A list of useful references are available at this website. What follows is some commented sample code. It represents my best understanding of SafeArrays and resource allocation, after struggling through the available documentation and source code.

bulletConstructing a SafeArray using SafeArrayPutElement
bulletReturning a SafeArray of VARIANT BSTR
bulletReturning a Two Dimensional SafeArrray
bulletLow Level Construction of a SafeArray of BYTES

Constructing a SafeArray using SafeArrayPutElements

You can put elements in a SafeArray using SafeArrayPutElements. This simplifies data access, but adds overhead to the array construction. A common requirement is to construct a SafeArray of VARIANTs of TYPE VT_BSTR. Such a SafeArray can then be returned to an ASP script as a VARIANT of TYPE VT_ARRAY | VT_VARIANT. This complex structure is required for compatibility with various scripting languages such as Visual Basic Script. Such a construct can also be used by JScript, but with more difficulty.

In the following code sample, I construct a SafeArray of ten elements. The sample converts the element index to an ANSI string using ltoa(). It then uses the ATL conversion macro A2W to convert the ANSI string to a Unicode "wide character" string. Finally, the Unicode string is "converted" to a BSTR by allocating memory for a length prefixed string on the system heap, returning a pointer to the string. This could also be accomplished using A2BSTR, but this macro hides the memory allocation that I am trying to demonstrate.

The important point here is that the BSTR exist in memory independent of the COM component. When returned as an [out, retval] it is the responsibility of the calling script engine to release the system memory holding the actual string data. You can look at a BSTR as calling new() in the COM component and calling delete() in the ASP script. Here is the verbose first pass at the code to create a SafeArray of BSTR VARIANTs:


USES_CONVERSION;  // enables use of ATL conversion macro A2W
char buffer[20];  // used to store ANSI string
HRESULT hr= S_OK;

// Create SafeArray of VARIANT BSTRs
SAFEARRAY *pSA;
SAFEARRAYBOUND aDim[1];    // a one dimensional array
aDim[0].lLbound= 0;  // Visual Basic arrays start with index 0
aDim[0].cElements= 10;
pSA= SafeArrayCreate(VT_VARIANT,1,aDim);  // create a 1D SafeArray of VARIANTS
if (pSA != NULL) {
    long aLong[1];
    // iterate over array adding VARIANTs of type VT_BSTR
    for (long l= aDim[0].lLbound; l< (aDim[0].cElements + aDim[0].lLbound); l++) {
        VARIANT vOut;
        VariantInit(&vOut);
        vOut.vt= VT_BSTR;  // set type
        ltoa(l,buffer,10);  // convert long to ANSI string value
        vOut.bstrVal= ::SysAllocString(A2W(buffer)); // system wide "new"
        aLong[0]= l;  // set index value
        if (hr= SafeArrayPutElement(pSA, aLong, &vOut)) { // "correctly" copies VARIANT
            VariantClear(&vOut);  // release BSTR from memory on error
            SafeArrayDestroy(pSA); // does a deep destroy on error
            return hr;
        }
        VariantClear(&vOut);  // does a deep destroy of source VARIANT
    } // end iteration
}
// clean up here only if you do not return SafeArray as an [out, retval]
SafeArrayDestroy(pSA); // again does a deep destroy

The key point here, at least according to my reading of the docs, is that SafeArrayPutElement "correctly" copies the VARIANT into the SafeArray, including presumably a deep copy of the BSTR. Any existing element in the SafeArray is released "correctly". To avoid a memory leak, you must call VariantClear() on the source VARIANT to reclaim the source BSTR memory after each PUT.

The class _variant_t is a wrapper class that automates memory management much like CComVariant. Using _variant_t greatly simplifies the above code since the overloaded assignment operator converts a wide character string to a BSTR.

_variant_t& operator=(const wchar_t* pSrc) throw(_com_error); // Assign a VT_BSTR

To use the _variant_t class you must add the #include <comdef.h> to the header file. I also eliminated the array of longs in this simplified version. I modeled the following code after Shelley Powers "Developing ASP Components Second Edition".


USES_CONVERSION;
char buffer[20];
HRESULT hr= S_OK;

// Create SafeArray of VARIANT BSTRs
SAFEARRAY *pSA;
SAFEARRAYBOUND aDim[1];
aDim[0].lLbound= 0;
aDim[0].cElements= 10;
pSA= SafeArrayCreate(VT_VARIANT,1,aDim);
if (pSA != NULL) {
    _variant_t vOut;
    for (long l= aDim[0].lLbound; l< (aDim[0].cElements + aDim[0].lLbound); l++) {
        ltoa(l,buffer,10);
        vOut= A2W(buffer);  // assignment operator automates memory allocation for BSTR
        if (hr= SafeArrayPutElement(pSA, &l, &vOut)) {
            SafeArrayDestroy(pSA); // does a deep destroy on error
            return hr;
        }
    }
}
// clean up here only if you do not return SafeArray as an [out, retval]
SafeArrayDestroy(pSA); // again does a deep destroy

 

Returning a SafeArray of VARIANT BSTRs

The following code demonstrates how to return the SafeArray of VARIANTs of type VT_BSTR constructed in the preceding section "Constructing A SafeArray Using SafeArrayPutElements", The following code takes a long as an input parameter and creates a SafeArray of VARIANTS of the corresponding length. The IDL looks like this:

[id(1), helpstring("method GetArray")] HRESULT GetArray([in] int newVal, [out, retval] VARIANT *pVal);

It is important to note the the out VARIANT *pVal is not initialized on entry to the method. If you call a method such as CComVariant.Detach(pVal), the out VARIANT will be cleared. Since the out VARIANT is not yet initialized, it contains random data. Clearing a VARIANT of random data is not recommended! The following code sample also calls the MACROS V_VT and V_ARRAY. These are poorly documented, but available for review in the header file oleauto.h.

Again, I modeled the C++ and ASP code after Shelley Powers "Developing ASP Components Second Edition".


// ASSERT newVal > 0
STDMETHODIMP CArray::GetArray(int newVal, VARIANT *pVal)
{
    // TODO: Add your implementation code here
    USES_CONVERSION;
    char buffer[20];
    HRESULT hr= S_OK;
    if (newVal < 1) {
        newVal= 1;
    }   

    // Create SafeArray of VARIANT BSTRs
    SAFEARRAY *pSA;
    SAFEARRAYBOUND aDim[1];
    aDim[0].lLbound= 0;
    aDim[0].cElements= newVal;
    pSA= SafeArrayCreate(VT_VARIANT,1,aDim);
    if (pSA != NULL) {
        _variant_t vOut;
        for (long l= aDim[0].lLbound; l< (aDim[0].cElements + aDim[0].lLbound); l++) {
            ltoa(l,buffer,10);
            vOut= A2W(buffer);
            if (hr= SafeArrayPutElement(pSA, &l, &vOut)) {
                SafeArrayDestroy(pSA); // does a deep destroy
                return hr;
            }
        }
    }

// return SafeArray as VARIANT
//VariantInit(pVal); // WARNING You must initialize *pVal before calling Detach
V_VT(pVal)= VT_ARRAY | VT_VARIANT; //pVal->vt= VT_ARRAY | VT_VARIANT; // oleauto.h
V_ARRAY(pVal)= pSA; // (pSA may be null) //pVal->parray= pSA; // oleauto.h
return S_OK;
}

You can call the code from VBScript like this:

<%@ LANGUAGE="VBScript" %>
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1">
<TITLE>Test Array DLL</TITLE>
</HEAD>
<BODY TopMargin="0" Leftmargin="0">
    <%
    Set objArray= Server.CreateObject("JALArray.Array")
    Dim arrayBSTR
    Dim x
    arrayBSTR= objArray.GetArray(10)
    for x= LBound(arrayBSTR) to UBound(arrayBSTR)
        Response.write "<BR>"&x&": "&arrayBSTR(x)
    next
    Set objArray= nothing
    %>
</BODY>
</HTML>

Calling the code from JScript is more complicated:

<%@ LANGUAGE="JSCRIPT" %>
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1">
<TITLE>Test Array DLL</TITLE>
</HEAD>
<BODY TopMargin="0" Leftmargin="0">
    <%
    var objArray= Server.CreateObject("JALArray.Array") ;
    var arrayVariants= new VBArray(objArray.GetArray(10));
    var arrayBSTR= arrayVariants.toArray();
    for (i= arrayVariants.lbound();i<= arrayVariants.ubound(); i++) {
        Response.write("<BR>"+i+": "+arrayBSTR[i]+"");
    }
    objArray= null;
    %>
</BODY>
</HTML>

The preceding JScript code first creates a VB array from the SafeArray. The call toArray() then returns a JScript array from the VB array.

Returning a Two Dimensional Array

Returning a rectangular two dimensional array is not that much different than the preceding code. Here is the two dimensioned version:

// ASSERT newVal > 0
STDMETHODIMP CArray::GetArray(int newVal, VARIANT *pVal)
{
    // TODO: Add your implementation code here
    USES_CONVERSION;
    char buffer[20];
    HRESULT hr= S_OK;
    if (newVal < 1) {
        newVal= 1;
    }

    // Create SafeArray of VARIANT BSTRs
    SAFEARRAY *pSA;
    SAFEARRAYBOUND aDim[2];    // two dimensional array
    aDim[0].lLbound= 0;
    aDim[0].cElements= newVal;
    aDim[1].lLbound= 0;
    aDim[1].cElements= newVal;    // rectangular array
    pSA= SafeArrayCreate(VT_VARIANT,2,aDim);  // again, 2 dimensions
    long aLong[2];
    long lSum;
    if (pSA != NULL) {
        _variant_t vOut;
        for (long x= aDim[0].lLbound; x< (aDim[0].cElements + aDim[0].lLbound); x++) {
            aLong[0]= x;    // set x index
            for (long y= aDim[1].lLbound; y< (aDim[1].cElements + aDim[1].lLbound); y++) {
                aLong[1]= y;    // set y index
                lSum= x+y;
                ltoa(lSum,buffer,10);
                vOut= A2W(buffer);
                if (hr= SafeArrayPutElement(pSA, aLong, &vOut)) {
                    SafeArrayDestroy(pSA); // does a deep destroy
                    return hr;
                }
            }
        }
    }   

    // return SafeArray as VARIANT
    //VariantInit(pVal); // WARNING You must initialize *pVal before calling Detach
    V_VT(pVal)= VT_ARRAY | VT_VARIANT; //pVal->vt= VT_ARRAY | VT_VARIANT;
    V_ARRAY(pVal)= pSA; // (pSA may be null) //pVal->parray= pSA; // UNION oleauto.h
    return S_OK;
}

Looping through a two dimensional array in the client script is a bit more complicated. Here is a VBScript sample that can process either a one or two dimensional array modified from "Programming MS Visual Basic 6.0" by Francesco Balena (MS Press).

<%@ LANGUAGE="VBScript" %>
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" content="text/html; charset=iso-8859-1">
<TITLE>Test MultiDimensionalArray DLL</TITLE>
</HEAD>
<BODY TopMargin="0" Leftmargin="0">
<%
    Set objArray= Server.CreateObject("JALArray.Array")
    Dim arrayBSTR
    Dim x
    arrayBSTR= objArray.GetArray(10)
    Select Case NumberOfDims(arrayBSTR)     ' Function defined below
         Case 1 'one dimensional array as table
            Response.write("<TABLE BORDER=1><TR>")
            for x= LBound(arrayBSTR) to UBound(arrayBSTR)
                Response.write "<TD>"&arrayBSTR(x)&"</TD>"
            next
            Response.write("</TR></TABLE>")
        Case 2 'two dimensional array as table
            Response.write("<TABLE BORDER=1>")
            for x= LBound(arrayBSTR) to UBound(arrayBSTR)
                Response.write("<TR>")
                for y= LBound(arrayBSTR,2) to UBound(arrayBSTR,2)
                    Response.write "<TD>"&arrayBSTR(x,y)&"</TD>"
                next
                Response.write("</TR>")
            next
            Response.write("</TABLE>")
          Case Else
            Err.Raise 1001    ' more than two dimensions ? not an array
    End Select
    Set objArray= nothing
%>
<SCRIPT LANGUAGE="VBScript" RUNAT="Server">
'Modified From Programming MS Visual Basic 6.0 Balena
Function NumberOfDims(array)
    Dim temp
    On Error Resume Next
    Do
        temp= UBound(array, NumberOfDims +1)
        If Err Then Exit Do
            NumberOfDims= NumberOfDims+1
        Loop
End Function
</SCRIPT>
</BODY>
</HTML>

 

Low Level Construction of a SafeArray of BYTES

While SafeArrayPutElement simplifies array access, it is not as efficient as directly accessing the underlying array. The following code demonstrates how to fill a SafeArray with BYTES of data using low level access and then write out the SafeArray using the Response object. This requires locking/unlocking of the SafeArray by calling SafeArrayAccessData and SafeArrayUnaccessData. Locking the SafeArray prevents the destruction of the SafeArray during manipulation. Calling SafeArrayDelete() on a locked SafeArray returns an error. In this sample, the raw data is stored in a array of char of size m_nChunkSize.

// Create char Array
char* out= new (nothrow) char[m_nChunkSize]; // Dynamic Allocation!
if(out == NULL) {
    return E_OUTOFMEMORY;
}

... code to fill char array removed for clarity

VARIANT vOut;
VariantInit(&vOut); // extra call

SAFEARRAY *pSA;
SAFEARRAYBOUND aDim[1];    // one dimensional array
aDim[0].lLbound= 0;
aDim[0].cElements= m_nChunkSize;
char * pActualData= NULL;
HRESULT hres= S_OK;


// Create SafeArray
pSA= SafeArrayCreate(VT_UI1,1,aDim);
if (pSA == NULL) {
    delete [] out;
    return E_OUTOFMEMORY;
}


try {
    VariantInit(&vOut); // may not be necessary
    vOut.vt= (VT_ARRAY|VT_UI1); // V_VT(&vOut)= (VT_ARRAY|VT_UI1);
    vOut.parray= pSA; // V_ARRAY(&vOut)= pSA;
    if (hres=SafeArrayAccessData(pSA,(void **)&pActualData)) throw hres;
    for (int i=0; i<m_nChunkSize; i++) {
        pActualData[i]= out[i];
    }
    if (hres=SafeArrayUnaccessData(pSA)) throw hres;
    m_piResponse->BinaryWrite(vOut);
catch(...) {
}

// Clean Up
delete [] out;
VariantClear(&vOut); // calls SafeArrayDestroy!
return hres;
}

 

Have fun,
Jeff

Send mail to [email protected] with questions or comments about this web site. Copyright 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009  
Last modified: 08/04/09
Hosted by www.Geocities.ws

1