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