Marzo de 2017

Volumen 32, número 3

C++: simplificar la programación segura de matrices en C++ con CComSafeArray

Por Giovanni Dicanio | Marzo de 2017

Es habitual desarrollar sistemas de software complejos mediante la compilación de componentes escritos en distintos lenguajes. Por ejemplo, podría tener código de C++ de alto rendimiento insertado en una interfaz C, una biblioteca de vínculos dinámicos (DLL) o un componente COM. O bien, puede escribir un servicio de Windows en C++, que exponga algunas interfaces COM. Los clientes escritos en C# pueden interactuar con ese servicio a través de la interoperabilidad de COM.

A menudo quiere intercambiar algunos datos en forma de matrices entre esos componentes. Por ejemplo, podría tener algunos componentes de C++ que interactúan con algunos componentes de hardware y producen una matriz de datos, como una matriz de bytes que representa los píxeles de una imagen leída desde un dispositivo de entrada, o una matriz de números de punto flotante que representa medidas leídas desde un sensor. O bien, podría tener un servicio de Windows escrito en C++ que interactúa con otros módulos de nivel bajo y devuelve matrices de cadenas que quiere usar en los clientes GUI escritos en C# o en un lenguaje de scripting. Pasar los datos a través de los límites del módulo no es trivial y requiere el uso de estructuras de datos bien diseñadas y creadas.

La plataforma de programación de Windows ofrece una práctica estructura de datos lista para usar, que puede emplearse con ese fin: SAFEARRAY, cuya definición se puede encontrar en el Centro de desarrollo de Windows (bit.ly/2fLXY6K). Básicamente, la estructura de datos SAFEARRAY describe una instancia concreta de una matriz segura, donde especifica atributos tales como el número de dimensiones, y un puntero a los datos de la matriz segura real. Una matriz segura suele controlarse en el código a través de un puntero a su descriptor de SAFEARRAY, que es SAFEARRAY*. También existen API de Windows de interfaces C para la manipulación de matrices seguras, como SafeArrayCreate y SafeArrayDestroy para la creación y destrucción, y otras funciones para bloquear una instancia de matriz segura y acceder con seguridad a sus datos. Para obtener más información sobre la estructura de datos C de SAFEARRAY y algunas de sus API de interfaces C nativas, consulte el escrito que complementa este artículo en línea, "Introducing the SAFE­ARRAY Data Structure" (Introducción de la estructura de datos SAFEARRAY) en msdn.com/magazine/mt778923.

No obstante, para los programadores de C++, en lugar de trabajar en el nivel de la interfaz C, resulta más práctico usar clases C++ de nivel superior, como CComSafeArray de Active Template Library (ATL).

En este artículo, analizaré ejemplos de código de C++ cada vez más complejos para crear matrices seguras que almacenen distintos tipos de datos mediante clases auxiliares de ATL, como CComSafeArray.

Matriz segura frente a vector STL

Las plantillas de clase de Standard Template Library (STL), como std::vector, son contenedores excelentes para el código de C++ dentro de los límites del módulo. Le recomiendo que las use en estos contextos. Por ejemplo, resulta más eficaz agregar contenido de forma dinámica y desarrollar std::vector que una matriz segura. Además, std::vector se integra fácilmente con algoritmos de STL y otras bibliotecas de C++ multiplataforma de terceros (como Boost). Asimismo, el uso de std::vector permite desarrollar código de C++ estándar multiplataforma; en su lugar, las matrices seguras son específicas de la plataforma Windows.

Por otro lado, cuando se imponen las matrices seguras, ello sucede en los límites del módulo. De hecho, los contenedores std::vector no pueden cruzar con seguridad los límites del módulo y no pueden consumirlos los clientes escritos en lenguajes distintos de C++. En su lugar, son los contextos exactos donde el uso de matrices seguras cobra sentido. Un buen patrón de codificación es aquel en el que ejecuta el procesamiento principal mediante contenedores de STL y C++ estándar, como std::vector. Posteriormente, cuando necesite transferir los datos de la matriz a través de los límites del módulo, puede proyectar el contenido de un contenedor std::vector en una matriz segura, que es un excelente candidato para cruzar los límites del módulo y para que consuman los clientes escritos en lenguajes distintos de C++.

Contenedor CComSafeArray de ATL

La interfaz de programación "nativa" de matrices seguras usa las API de interfaces C de Win32, como se describe en el escrito que complementa este artículo en línea. Aunque es posible usar las funciones de C en el código de C++, estas tienden a producir código complejo y propenso a errores; por ejemplo, debe prestar atención a hacer coincidir correctamente las desasignaciones de matrices seguras con cada asignación de matriz segura a fin de hacer coincidir correctamente bloqueos con desbloqueos, etc.

Afortunadamente, con C++ se puede simplificar un poco ese código de estilo C mediante prácticos patrones de codificación, como RAII y destructores. Por ejemplo, puede escribir una clase para encapsular descriptores SAFEARRAY sin formato y, a continuación, hacer que el constructor llame a SafeArrayLock y el destructor llame a la función SafeArrayUnlock coincidente. De este modo, la matriz segura se desbloquea automáticamente cuando el objeto contenedor queda fuera de ámbito. También tiene sentido definir esta clase como una plantilla para aplicar la seguridad de tipos en la matriz segura. De hecho, mientras que en C la genericidad de la matriz segura se expresa mediante un puntero void para el campo SAFEARRAY::pvData, en C++ se puede mejorar la comprobación de tipos en tiempo de compilación por medio de plantillas.

Afortunadamente, no tiene que escribir esta plantilla de clase desde cero. De hecho, ATL ya proporciona una práctica plantilla de clase de C++ para simplificar la programación de matrices seguras: CComSafeArray, declarada en el encabezado <atlsafe.h>. En la clase CComSafeArray<T> de ATL, el parámetro de plantilla T representa el tipo de datos almacenados en la matriz segura. Por ejemplo, para una matriz segura de BYTEs, usaría CComSafeArray<BYTE>; una matriz segura de propiedades float está encapsulada mediante CComSafeArray<float> y así sucesivamente. Tenga en cuenta que la matriz segura encapsulada interna sigue siendo una matriz polimórfica de estilo C basada en un puntero void. No obstante, la capa de encapsulado de C++ que compila CComSafeArray ofrece un nivel más alto y seguro de abstracción, que incluye administración del ciclo de vida de la matriz segura y seguridad de tipos superior que void* de C.

CComSafeArray<T> solo tiene un miembro de datos, m_psa, que es un puntero a un descriptor de matriz segura (SAFEARRAY*) encapsulado por el objeto CComSafeArray.

Construcción de instancias de CComSafeArray

El constructor predeterminado CComSafeArray crea un objeto contenedor, que contiene solo un puntero nulo a un descriptor de matriz segura; básicamente, no encapsula nada.

Además, existen varias sobrecargas de constructores que pueden venir bien. Por ejemplo, puede pasar un recuento de elementos para crear una matriz segura formada por un número determinado de elementos:

// Create a SAFEARRAY containing 1KB of data
CComSafeArray<BYTE> data(1024);

También es posible especificar un límite inferior distinto de cero, que es el predeterminado:

// Create a SAFEARRAY containing 1KB of data
// with index starting from 1 instead of 0
CComSafeArray<BYTE> data(1024, 1);

No obstante, al escribir código para actuar en matrices seguras que clientes de C++ o C#/.NET deban procesar, es mejor seguir la convención habitual consistente en tener un límite inferior de cero (en lugar de uno).

Tenga en cuenta que los constructores CComSafeArray llaman automáticamente a SafeArrayLock por usted; así, la matriz segura encapsulada ya está bloqueada y lista para las operaciones de lectura y escritura de código del usuario.

Si tiene un puntero a un descriptor de matriz segura existente, puede pasarlo a otra sobrecarga de constructor CComSafeArray:

// psa is a pointer to an existing safe array descriptor
// (SAFEARRAY* psa)
CComSafeArray<BYTE> sa(psa);

En este caso, CComSafeArray intenta crear una copia en profundidad de la matriz segura original de BYTEs señalada por "psa".

Limpieza de recursos automática con CComSafeArray

Si un objeto CComSafeArray queda fuera de ámbito, su destructor limpiará automáticamente la memoria y los recursos asignados por la matriz segura encapsulada. Esto resulta muy práctico y simplifica enormemente el código de C++. De este modo, los programadores de C++ pueden centrarse en el núcleo de su código y no en los detalles de la limpieza de memoria de la matriz segura, lo que evita los molestos errores de fuga de memoria.

Si explora el código CComSafeArray de ATL, verá que el destructor CComSafeArray llama primero a SafeArrayUnlock y, luego, a SafeArrayDestroy, por lo que no se necesita código de cliente para desbloquear y destruir explícitamente la matriz segura; en realidad, eso sería un error de destrucción doble.

Métodos prácticos para las operaciones de SafeArray comunes

Además de la administración automática del ciclo de vida de la matriz segura, la plantilla de clase CComSafeArray ofrece algunos métodos prácticos para simplificar las operaciones en las matrices seguras. Por ejemplo, puede llamar simplemente al método GetCount para obtener el recuento de elementos de la matriz segura encapsulada. Para obtener acceso a los elementos de la matriz segura, puede llamar a los métodos CComSafeArray::GetAt y SetAt, y especificar simplemente el índice del elemento.

Por ejemplo, para la iteración por los elementos de una matriz segura existente, puede usar código como el siguiente:

// Assume sa is a CComSafeArray instance wrapping an existing safe array.
// Note that this code is generic enough to handle safe arrays
// having lower bounds different than the usual zero.
const LONG lb = sa.GetLowerBound();
const LONG ub = sa.GetUpperBound();
// Note that the upper bound is *included* (<=)
for (LONG i = lb; i <= ub; i++)
{
  // ... use sa.GetAt(i) to access the i-th item
}

Además, CComSafeArray sobrecarga operator[] para ofrecer una sintaxis aún más simple para acceder a los elementos de la matriz segura. Verá algunos de estos métodos en acción en las siguientes secciones de este artículo.

También es posible anexar nuevos elementos a una matriz segura existente, e invocar el método CComSafeArray::Add. Además, CCom­SafeArray::Resize, como su nombre sugiere claramente, puede usarse para cambiar el tamaño de la matriz segura encapsulada. No obstante, en general, sugeriría escribir código de C++ que actúe en std::vector, que presenta más rapidez que las matrices seguras para cambiar de tamaño, y se integra bien con otros algoritmos de STL y también con otras bibliotecas de C++ multiplataforma de terceros. A continuación, al terminar las operaciones en el contenido de std::vector, los datos se pueden copiar en una matriz segura, que puede cruzar con seguridad los límites del módulo (a diferencia de std::vector), y también pueden consumirlos los clientes escritos en lenguajes distintos de C++.

Las operaciones de copia de matriz segura son posibles mediante la función de asignación de copia sobrecargada operator= o la invocación de métodos, como CComSafeArray::CopyFrom.

"Semántica de transferencia de recursos" en CComSafeArray

La clase CComSafeArray no implementa la "semántica de transferencia de recursos" en sentido estricto de C++11; de hecho, esta clase ATL es anterior a C++11 y, por lo menos hasta Visual Studio 2015, las operaciones de movimiento de C++11, como el constructor de movimiento y el operador de asignación de movimiento, no se incluyen. No obstante, existe un tipo de semántica de transferencia de recursos diferente que ofrece CComSafeArray y que se basa en un par de métodos: Attach y Detach. Básicamente, si tiene un puntero a un descriptor de matriz segura (SAFEARRAY*), puede pasarlo al método Attach, y CComSafeArray tomará la propiedad de la matriz segura sin procesar. Tenga en cuenta que cualquier dato de matriz segura encapsulado en el objeto CComSafeArray existente anteriormente se limpiará correctamente.

De manera similar, puede llamar al método CComSafeArray::Detach, con lo cual el contenedor CComSafeArray liberará la propiedad de la matriz segura encapsulada al autor de la llamada y devolverá un puntero al descriptor de la matriz segura que poseía anteriormente. El método Detach resulta práctico cuando crea una matriz segura en código de C++ mediante CComSafeArray y, luego, lo entrega como un parámetro de puntero de salida al autor de una llamada, por ejemplo en un método de interfaz COM o una función DLL de interfaz C. De hecho, la clase CComSafeArray C++ no puede cruzar los límites de las funciones DLL de interfaces COM ni C; solo pueden hacerlo los punteros del descriptor SAFEARRAY*.

Las excepciones de C++ no pueden cruzar los límites de DLL de COM ni C

Algunos de los métodos CComSafeArray, como Create, CopyFrom, SetAt, Add y Resize, devuelven valores HRESULT para las condiciones de éxito o error de la señal, como suele suceder en la programación de COM. No obstante, otros métodos, como algunas sobrecargas de constructor CComSafeArray, o GetAt u operator[], inician excepciones realmente sobre los errores. De manera predeterminada, ATL inicia excepciones de C++ de tipo CAtlException, que es un contenedor diminuto en torno a un valor HRESULT.

No obstante, las excepciones de C++ no pueden cruzar los límites de las funciones DLL de métodos COM o interfaces C. Para los métodos COM, es obligatorio devolver valores HRESULTs a los errores de señal. Esta opción también se puede usar en las DLL de interfaces C. Así, al escribir código de C++ que use el contenedor CComSafeArray (o cualquier otro componente de lanzamiento), es importante proteger ese código mediante un bloque try/catch. Por ejemplo, dentro de la implementación de un método COM, o dentro de una función de límite DLL que devuelva un valor HRESULT, puede escribir código como el siguiente:

try
{
  // Do something that can potentially throw exceptions as CAtlException ...
}
catch (const CAtlException& e)
{
  // Exception object implicitly converted to HRESULT,
  // and returned as an error code to the caller
  return e;
}
// All right
return S_OK;

Producción de una matriz segura de bytes

Después del marco conceptual anterior en el que se presentaban las matrices seguras y el contenedor ATL CComSafeArray de C++, me gustaría analizar algunas aplicaciones prácticas de la programación de matrices seguras mostrando CComSafeArray en acción. Comenzaré con un caso simple: producir una matriz segura de bytes a partir de código de C++. Esta matriz segura se puede pasar como un parámetro de salida en un método de interfaz COM o una función DLL de interfaz C.

Se suele hacer referencia a una matriz segura a través de un puntero a su descriptor, que se traslada a SAFEARRAY* en el código de C++. Si tiene un parámetro de matriz segura de salida, necesita otro nivel de direccionamiento indirecto, de modo que una matriz segura pasada como un parámetro de salida tenga el formato SAFEARRAY** (doble puntero al descriptor SAFEARRARY):

STDMETHODIMP CMyComComponent::DoSomething(
    /* [out] */ SAFEARRAY** ppsa)
{
  // ...
}

Dentro del método COM, no olvide usar una restricción try/catch para capturar excepciones y trasladarlas a los valores HRESULT correspondientes, como se muestra en el párrafo anterior. (Tenga en cuenta que la macro de preprocesador STDMETHODIMP implica que el método devuelve un valor HRESULT).

Dentro del bloque try, se crea una instancia de la plantilla de clase CComSafeArray y se especifica BYTE como parámetro de plantilla:

// Create a safe array storing 'count' BYTEs
CComSafeArray<BYTE> sa(count);

A continuación, puede acceder simplemente a los elementos de la matriz segura mediante la función operator[] sobrecargada de CComSafeArray; por ejemplo:

for (LONG i = 0; i < count; i++)
{
  sa[i] = /* some value */;
}

Después de escribir sus datos completamente en la matriz segura (por ejemplo, mediante la copia desde un contenedor std::vector), la matriz segura se puede devolver como un parámetro de salida al invocar el método Detach de CComSafeArray:

*ppsa = sa.Detach();

Tenga en cuenta que después de invocar el método Detach, el objeto contenedor CComSafeArray transfiere la propiedad de la matriz segura al autor de la llamada. Así, cuando el objeto CComSafeArray queda fuera de ámbito, la matriz segura creada en el código anterior no se destruye. Gracias al método Detach, los datos de la matriz segura solo se transferían o "trasladaban" del código anterior que creó la matriz segura en primer lugar al autor de la llamada. Ahora, es responsabilidad del autor de la llamada destruir la matriz segura cuando ya no sea necesaria.

Las matrices seguras también son prácticas para intercambiar datos de matriz entre los límites de módulos DLL. Piense, por ejemplo, en una DLL de interfaz C escrita en C++, que produce una matriz segura que se consume en algún código de cliente de C#. Si la función de límite que exporta la DLL tiene este prototipo:

extern "C" HRESULT __stdcall ProduceByteArrayData(/* [out] */ SAFEARRAY** ppsa)

la declaración PInvoke de C# correspondiente es:

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void ProduceByteArrayData(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
  out byte[] result);

UnmanagedType.SafeArray significa que el tipo de matriz nativa real en el límite de la función es SAFEARRAY. El valor Var­Enum.VT_UI1 asignado a SafeArraySubType especifica que el tipo de datos almacenados en la matriz segura es BYTE (que es un entero sin signo de un byte de tamaño exactamente). Para una matriz segura que almacene enteros con signo de 4 bytes, en el lado de C++ tendría CComSafe­Array<int> y el tipo VarEnum de PInvoke correspondiente sería VT_I4 (que significa "entero con signo de 4 bytes de tamaño). La matriz segura está asignada a una matriz byte[] en C#, que se pasa como un parámetro de salida.

El atributo PreserveSig = false indica a PInvoke que traslade los valores de error HRESULT devueltos por la función nativa a las excepciones de C#.

En la Figura 1 se muestra un fragmento de código de ejemplo completo para producir una matriz segura de bytes en C++ mediante CComSafeArray. El código forma parte de un método COM hipotético; no obstante, el mismo código basado en CComSafeArray puede usarse también para las funciones de límite de DLL de interfaces C.

Figura 1 Producción de una matriz segura de bytes mediante CComSafeArray

STDMETHODIMP CMyComComponent::DoSomething(/* [out] */ SAFEARRAY** ppsa) noexcept
{
  try
  {
    // Create a safe array storing 'count' BYTEs
    const LONG count = /* some count value */;
    CComSafeArray<BYTE> sa(count);
    // Fill the safe array with some data
    for (LONG i = 0; i < count; i++)
    {
      sa[i] = /* some value */;
    }
    // Return ("move") the safe array to the caller
    // as an output parameter
    *ppsa = sa.Detach();
  }
  catch (const CAtlException& e)
  {
    // Convert ATL exceptions to HRESULTs
    return e;
  }
  // All right
  return S_OK;
}

Producción de una matriz segura de cadenas

Ahora ya sabe cómo crear una matriz segura de bytes e incluso la signatura de declaración PInvoke que se usará en C# al pasar la matriz segura como un parámetro de salida en una función DLL de interfaz C. Este patrón de codificación funciona bien con las matrices seguras que almacenan otros tipos escalares, como int, float, double, etc. Esos tipos tienen las mismas representaciones binarias en C++ y C#, y se serializan fácilmente entre estos dos mundos y también a través de los límites de los componentes COM.

No obstante, es necesario prestar atención adicional a las matrices seguras que almacenan cadenas. Las cadenas exigen un cuidado especial, ya que son más complejas que los escalares únicos, tales como bytes, enteros o números de punto flotante. BSTR es un tipo práctico que se usa para representar cadenas que pueden cruzar con seguridad los límites del módulo. Aunque este tipo es bastante versátil, puede considerarlo un puntero de cadena Unicode UTF-16 prefijado con la longitud. El administrador de serialización predeterminado sabe cómo copiar tipos BSTR y cómo hacer que crucen los límites de función de las interfaces COM o C. Una descripción detallada e interesante de la semántica de BSTR se puede leer en la entrada en el blog de Eric Lippert en bit.ly/2fLXTfY.

Para crear una matriz segura que almacene cadenas en C++, se puede crear una instancia de la plantilla de clase CComSafeArray mediante el tipo BSTR:

// Creates a SAFEARRAY containing 'count' BSTR strings
CComSafeArray<BSTR> sa(count);

Aunque el tipo de matriz segura especificado aquí es el tipo BSTR C sin procesar, en el código de C++ es mucho mejor (es decir, más fácil y seguro) usar un contenedor RAII alrededor de los tipos BSTR sin procesar. ATL ofrece un práctico contenedor de este tipo en forma de clase CComBSTR. En el código de Win32/C++ compilado con Microsoft Visual C++ (MSVC), las cadenas Unicode UTF-16 se pueden representar mediante la clase std::wstring. De este modo, tiene sentido compilar una práctica función auxiliar para la conversión de std::wstring en ATL::CComBSTR:

// Convert from STL wstring to the ATL BSTR wrapper
inline CComBSTR ToBstr(const std::wstring& s)
{
  // Special case of empty string
  if (s.empty())
  {
    return CComBSTR();
  }
  return CComBSTR(static_cast<int>(s.size()), s.data());
}

Para rellenar una matriz CComSafeArray<BSTR> con cadenas copiadas de un vector<wstring> de STL, es posible la iteración a través del vector y, para cada wstring del vector, se puede crear un objeto CComBSTR correspondiente invocando la función auxiliar mencionada anteriormente:

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

A continuación, se puede invocar el método CComSafeArray::SetAt para copiar la cadena bstr devuelta en el objeto de matriz segura:

hr = sa.SetAt(i, bstr);

El método SetAt devuelve un valor HRESULT, por lo que resulta una buena práctica de programación comprobar su valor e iniciar una excepción en caso de errores:

if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

La excepción se convertirá en un valor HRESULT en el método COM o en el límite de la función DLL de la interfaz C. Como alternativa, el valor HRESULT erróneo se podría devolver directamente desde el fragmento de código anterior.

La principal diferencia de este caso de matriz segura BSTR en relación con la muestra CComSafeArray<BYTE> anterior es la creación de un objeto contenedor CComBSTR intermedio alrededor de las cadenas BSTR. Estos contenedores no necesitan tipos escalares simples, como bytes, enteros o números de punto flotante; no obstante, para otros tipos más complejos, como BSTR, que requieren una administración del ciclo de vida más correcta, resultan útiles esos contenedores RAII de C++. Por ejemplo, los contenedores como CComBSTR ocultan las llamadas a funciones tales como SysAllocString, que se usa para crear un nuevo tipo BSTR a partir de un puntero de cadena de estilo C existente. De manera similar, el destructor CComBSTR llama automáticamente a SysFreeString para liberar la memoria de BSTR. Todos estos detalles de administración del ciclo de vida de BSTR se ocultan convenientemente dentro de la implementación de clase CCom­BSTR, de modo que los programadores de C++ puedan centrar su atención en la lógica de nivel superior del código.

Optimización de la "semántica de transferencia de recursos" para las matrices seguras de BSTR

Observe que la llamada al método CComSafeArray<BSTR>::SetAt anterior realiza una copia en profundidad del tipo BSTR de entrada en la matriz segura. De hecho, el método SetAt tiene un tercer parámetro BOOL bCopy adicional, que se establece de manera predeterminada en TRUE. Este parámetro de marca bCopy no es importante para los tipos escalares, como bytes, enteros o números de punto flotante, ya que todos están copiados en profundidad en la matriz segura. No obstante, es importante para tipos más complejos, como BSTR, en los que la administración del ciclo de vida y la semántica de copia no son triviales. Por ejemplo, en este caso de CComSafeArray<BSTR>, si FALSE se especifica como tercer parámetro para SetAt, la matriz segura simplemente toma la propiedad del tipo BSTR de entrada, en lugar de copiarlo en profundidad. Es una especie de operación de movimiento rápida optimizada, en lugar de una copia en profundidad. Esta optimización también requiere que el método Detach se invoque en el contenedor CComBSTR para transferir correctamente la propiedad de BSTR de CComBSTR a CComSafeArray:

hr = sa.SetAt(i, bstr.Detach(), FALSE);

Como ya hemos visto en la muestra CComSafeArray<BYTE>, al completar la creación de CComSafeArray<BSTR>, la matriz segura se puede pasar (mover) al autor de la llamada con código como el siguiente:

// Return ("move") the safe array to the caller
// as an output parameter (SAFEARRAY **ppsa)
*ppsa = sa.Detach();

Una función DLL de interfaz C que compila una matriz segura de cadenas BSTR y la pasa al autor de la llamada se puede invocar con PInvoke en C# de la siguiente manera:

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void BuildStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)]
  out string[] result);

Observe el uso de VarEnum.VT_BSTR para indicar la presencia de una matriz segura que almacena cadenas BSTR. La matriz SAFEARRAY de cadenas BSTR que se produce en el código de C++ nativo se serializa en C# mediante un tipo de matriz string[], que se pasa como un parámetro de salida.

Producción de una matriz segura de variantes que contienen cadenas

Vamos a ir un paso más allá para aumentar el nivel de complejidad. Además de una matriz segura que almacene elementos de tipos tales como byte, entero y cadenas BSTR, también se puede crear una matriz segura de tipo "genérico": el tipo VARIANT. Una variante es un tipo polimórfico que puede almacenar valores de una gran variedad de tipos diferentes, que van desde enteros hasta números de punto flotante, cadenas BSTR, etc. El tipo C VARIANT es básicamente una unión gigantesca; su definición se puede encontrar en bit.ly/2fMc4Bu. Igual que para el tipo BSTR C, ATL ofrece un práctico contenedor de C++ alrededor del tipo C VARIANT: la clase ATL::CComVariant. En el código de C++, es más fácil y seguro controlar variantes mediante el contenedor CComVariant, en lugar de invocar directamente funciones C para asignar, copiar y borrar variantes.

Mientras que los clientes escritos en C++ y C# comprenden que las matrices seguras almacenan tipos "directos" (como se muestra en los ejemplos anteriores de BYTE y BSTR), existen algunos clientes de scripting que solo comprenden las matrices seguras que almacenan variantes. Por tanto, si quiere compilar datos de matriz en C++ y hacer que estos clientes de scripting puedan consumirlos, estos datos deben empaquetarse en una matriz segura que almacene variantes para agregar un nuevo nivel de direccionamiento indirecto (así como alguna sobrecarga adicional). A su vez, cada elemento de variante de la matriz segura almacenará un valor de BYTE, un número de punto flotante, una cadena BSTR o un valor de cualquier tipo compatible.

Supongamos que quiere modificar el código anterior que compila una matriz segura de cadenas BSTR usando una matriz segura de variantes en su lugar. A su vez, las variantes contendrán cadenas BSTR, pero lo que se producirá es una matriz segura de variantes, no una matriz segura directa de cadenas BSTR.

En primer lugar, para crear una matriz segura de variantes, se puede crear una instancia de la plantilla de clase CComSafeArray de la siguiente manera:

// Create a safe array storing VARIANTs
CComSafeArray<VARIANT> sa(count);

A continuación, puede realizar la iteración a través de un conjunto de cadenas almacenadas, como, por ejemplo, en un vector<wstring> de STL. Para cada wstring, puede crear un objeto CComBSTR, como en el ejemplo de código anterior:

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

Ahora, surge una diferencia del caso anterior de matrices seguras de BSTR. De hecho, dado que esta vez está compilando una matriz segura de VARIANT (no una matriz segura directa de BSTR), no puede almacenar el objeto BSTR directamente en CComSafeArray llamando a SetAt. En su lugar, se debe crear primero un objeto variant para encapsular la cadena bstr; luego, ese objeto variant se puede insertar en la matriz segura de variantes:

// First create a variant from the CComBSTR
  CComVariant var(bstr);
  // Then add the variant to the safe array
  hr = sa.SetAt(i, var);
  if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

Observe que CComVariant tiene un constructor sobrecargado que toma un puntero const wchar_t*, lo que permitiría compilar directamente un contenedor CComVariant a partir de std::wstring e invocar el método wstring c_str. No obstante, en este caso, la variante almacenará solo el fragmento inicial del tipo wstring original hasta el primer terminador NUL; mientras tanto, tanto wstring como BSTR pueden almacenar cadenas con valores NUL insertados. Por lo tanto, la compilación de un objeto intermedio CComBSTR a partir de la clase std::wstring, como hicimos anteriormente en la función auxiliar ToBstr personalizada, también trata este caso más genérico.

Como es habitual, para devolver la matriz segura creada al autor de la llamada como un parámetro de salida SAFEARRAY**, se puede usar el método CComSafeArray::Detach:

// Transfer ownership of the created safe array to the caller
*ppsa = sa.Detach();

Si la matriz segura se pasa a través de una función de interfaz C como esta:

extern "C" HRESULT __stdcall BuildVariantStringArray(/* [out] */ SAFEARRAY** ppsa)

Se puede usar la siguiente declaración PInvoke de C#:

[DllImport("NativeDll.dll", PreserveSig = false)]
pubic static extern void BuildVariantStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
  out string[] result);

Observe el uso de VarEnum.VT_VARIANT para SafeArraySubType, dado que esta vez la matriz segura creada en C++ contiene variantes (que, a su vez, encapsulan cadenas BSTR), pero no BSTR directamente.

En general, recomendaría exportar datos mediante matrices seguras de tipos directos, en lugar de matrices seguras con variantes, salvo que tenga restricciones. Para ello, haga que sus datos sean accesibles para los clientes de scripting que solo pueden controlar la matriz segura de variantes.

Resumen

La estructura de datos de matriz segura es una práctica herramienta para intercambiar datos de matrices entre distintos límites de lenguajes y módulos. La matriz segura es una estructura de datos versátil, que se puede almacenar en tipos primitivos simples, como bytes, enteros o números de punto flotante, pero también tipos más complejos, como cadenas BSTR o incluso estructuras VARIANT genéricas. Este artículo le mostró cómo simplificar la programación de estas estructuras de datos en C++ con casos de muestra concretos mediante clases auxiliares de ATL.


Giovanni Dicanio es un programador informático especializado en C++ y en el sistema operativo Windows, autor de Pluralsight (bit.ly/GioDPS) y MVP de Visual C++. Además de la programación y la creación de cursos, disfruta ayudando a los demás en foros y comunidades dedicadas a C++. Puede ponerse en contacto con él en la dirección giovanni.dicanio@gmail.com. También escribe en el blog en msmvps.com/gdicanio.

Gracias a los siguientes expertos técnicos por revisar este artículo: David Cravey y Marc Gregoire
David Cravey trabaja como arquitecto empresarial en GlobalSCAPE, dirige varios grupos de usuarios de C++ y ha sido cuatro veces MVP de Visual C++.
Marc Gregoire es un ingeniero de software senior de Bélgica, fundador del grupo belga de usuarios de C++, autor de "Professional C++" (Wiley), coautor de "C++ Standard Library Quick Reference" (Apress), editor técnico en numerosos libros y, desde 2007, ha recibido el premio anual MVP por sus conocimientos en VC++. Puede ponerse en contacto con Marc en marc.gregoire@nuonsoft.com.