AdpService para .NET
Descargas relacionadas
Desde el primer anuncio de la plataforma AppUp para desarrolladores, se ha generado un interés significativo en crear aplicaciones en .NET. A primeros de diciembre, yllian-saint-hilare escribió un artículo en su blog acerca de Cómo crear aplicaciones .NET para la tienda. El artículo proporcionaba los pasos necesarios para crear un contenedor .NET alrededor de una DLL de C++ no gestionada para el SDK de ADP. Esto hizo que se incluyesen dos DLL en la aplicación .NET. Este artículo empieza a partir del trabajo de Yllian, pero reescribe y completa el código para producir una sola DLL gestionada que engloba todo el SDK de ADP.
Nota: Esta descripción y el código que la acompaña corresponden al SDK de ADP de Intel versión 0.92.
Sólo quiero el código.
Si únicamente desea descargar la DLL y utilizarla para crear o conectar su aplicación a ADP, vea el enlace que se encuentra a la derecha. Hay un archivo zip que contiene la DLL binaria que puede arrastrar y soltar sobre su aplicación para utilizarla.
1. Cree una nueva aplicación Windows Forms

2. Copie la DLL en la carpeta de solución de su aplicación (o, mejor aún, en una carpeta ‘lib’ de la carpeta de solución).
3. Navegue hasta el archivo DLL para agregar una referencia en Visual Studio.

4. Cree una instancia de un objeto com.intel.adp.AdpWrapper en el código de inicio de su aplicación y llame, como mínimo, a Initialize y a IsAuthorized.
Para obtener más detalles sobre cómo crear una aplicación utilizando este código, consulte el artículo Aplicación de ejemplo de Facebook en .NET.
Creación de la DLL de .NET para ADP
La explicación siguiente se basa en el trabajo de Yllian, sin el cual esto no habría sido posible. Espero proporcionar ideas claras por si desea crear y configurar la biblioteca personalmente, mejorarla o actualizarla con versiones más recientes del SDK de ADP.
Para ayudar a aclarar el tema, sólo voy a resaltar partes del código. Puede descargar la solución entera desde el enlace situado a la derecha y puede revisar el código mientras lee este texto.
Las bibliotecas SDK de ADP se entregan como archivos .lib concebidos para crear un enlace estático a su proyecto. Para que .NET tenga acceso al código no gestionado, tiene que poder hacer referencia a una DLL. Puede enlazar código no gestionado a una DLL gestionada (es decir, .NET) de manera estática, pero sólo si escribe en C++ gestionado.
Creación de una DLL gestionada para que haga referencia a la biblioteca de ADP
Partiendo desde cero, abrí Visual Studio 2008 y creé un proyecto “C++ CLR Class Library” nuevo denominado AdpService. Tenga en cuenta que es CLR (Common Language Runtime) que permite que las aplicaciones .NET consuman la DLL.

Ya que voy a exponer un método gestionado para cada función del SDK de ADP, copié adpcode.lib y adpcode.h de la carpeta SDK en la carpeta donde se encuentra este proyecto para que el vinculador pueda encontrarla. Utilicé la API de C ya que es más fácil de utilizar que la de C++, pero expondré las llamadas del SDK mediante una clase gestionada.
Para asegurarme de que estuvieran las referencias al proyecto, agregué “adpcore.h” como inclusión en el archivo AppService.h y como elemento existente en los archivos de cabecera del proyecto.
Por último, debemos indicarle al vinculador que busque la biblioteca. En las propiedades del proyecto, asegúrese de seleccionar “Todas las configuraciones” en la esquina superior derecha. A continuación, seleccione Linker/Input y agregue “adpcore.lib psapi.lib shlwapi.lib Advapi32.lib” (necesitará las cuatro) a la entrada “Additional Dependencies”.

Creación de una clase de .NET para acceder al sistema ADP
Para convertir de .NET a código no gestionado (adpcore.lib), vamos a tener que duplicar cada función, typedef y enum y calcular las referencias de los datos entre tipos gestionados y no gestionados1.
Creé una ‘clase public enum’, ReturnCode, en el archivo de cabecera que replica la enum que está en el archivo de cabecera C, adpcore.h. Esto calculará las referencias directamente, ya que enum representa un entero con signo. Cada llamada a las funciones de la biblioteca C devolverá un valor que podemos definir como tipo enum.
Las funciones de la biblioteca C podrían ser métodos estáticos en la clase de .NET; no obstante, para probar nuestro código con la integración con ADP, vamos a querer crear un servicio ADP ficticio, por lo que lo convertiremos en una clase con método de instancia y que tenga una interfaz detrás.
Si examina AdpService.h en la fuente, verá que todas las llamadas a métodos y las dos propiedades que no son constantes no son estáticas.
public ref class AdpService : IAdpService
{
public:
virtual ReturnCode Initialize();
virtual ReturnCode Close();
virtual ReturnCode ReportCrash( ... );
virtual ReturnCode IsAuthorized(Guid applicationid);
virtual ReturnCode IsAppAuthorized(Guid componentid);
virtual ReturnCode ApplicationBeginEvent();
virtual ReturnCode ApplicationEndEvent();
virtual property String^ ApiVersion{ String^ get(); };
virtual property unsigned long ApiLevel{ unsigned long get(); };
property static Guid DebugApplicationId
{
...
}
property static Guid DebugComponentId
{
....
}
};
Si está familiarizado con la API de C++ existente en el SDK, observará que podría heredar de esta clase o utilizar un objeto de la misma. A diferencia del SDK, no proporciona inicialización ni autorización inmediatamente mediante un constructor. Si lo desea, podría agregar esto, pero es muy recomendable que componga (o delegue) la integración con ADP de la clase de su programa principal en el objeto n AdpService por tres motivos:
- 1. Su aplicación no es un servicio de autorización, por lo que en realidad no debe convertirlo en una subclase basándose solamente en el contexto del dominio.
- 2. Utilizar subclases puede causar que el constructor superior genere una excepción que no se gestione adecuadamente (obtendrá un objeto iniciado de manera inadecuada). Existe un mensaje detallado en el foro sobre este tema.
- 3. La composición le permite utilizar la inyección de dependencia y sustituirla en su aplicación con un objeto ficticio durante las pruebas, para que pueda automatizar las pruebas sin ejecutar ATDS.
Conversión de los GUID
El SDK utiliza unos GUID para los ID de aplicación y componente. Dado que son unos GUID estándar y .NET tiene un tipo incorporado, System.Guid, parecía una buena idea utilizar el tipo incorporado2. Los dos métodos de autorización aceptan tipos de GUID en su firma y creé dos propiedades para devolver tipos de GUID para los ID de depuración utilizados para el desarrollo y prueba de aplicaciones y componentes.
property static Guid DebugApplicationId
{
Guid get()
{
array<unsigned char>^ id = gcnew array<unsigned char>(16);
pin_ptr<unsigned char> dst = &id[0];
memcpy(dst, &ADP_DEBUG_APPLICATIONID, 16);
return Guid(id);
}
}
El código anterior nos permite obtener datos no gestionados en memoria gestionada y convertirlos en un tipo. En primer lugar, utilicé System.Guid como tipo de retorno de valor (o sea, no como una referencia, “Guid^”) porque es una estructura, no una clase.
En el captador, creamos una matriz de bytes nueva en la memoria gestionada (‘gcnew’ significa que se recolecta como elemento no utilizado). Si no está familiarizado con C++ gestionado de Microsoft, ^ es equivalente a * (es decir, indica una referencia), pero se utiliza para las referencias gestionadas.
A continuación, fijamos un puntero al inicio de nuestra matriz. Esto indica al calculador de referencias que deseamos utilizar la misma memoria en la llamada no gestionada para poder obtener los resultados cuando hayamos terminado.
A continuación, utilizamos la función estándar de C memcpy para copiar, byte a byte, el valor contenido en el puntero ADP_DEBUG_APPLICTIONID de la biblioteca de C en nuestra matriz de bytes. Tenga en cuenta que tuve que agregar
Por último, creamos un tipo Guid pasando la matriz de bytes al constructor.
Es importante señalar aquí que sé que los datos de ADP_DEBUG_APPLICATIONID tienen 16 bytes de longitud. Si crea esto con una versión diferente de la API de C, debe comprobarlo o cambiar el código como corresponda.
Cálculo de referencias de datos
A partir de las firmas de métodos mostradas anteriormente, verá que la mayoría de llamadas no tienen datos entrantes, lo que facilita nuestro trabajo de conversión. Es aún más útil que todos los datos que entren en las llamadas sean constantes, es decir, que las funciones no cambien los datos. Esto facilita el cálculo de referencias de datos, porque no tenemos que fijarlos para nuestras llamadas de API. (Podemos dejar que el calculador de referencias realice copias de los datos para utilizarlas en código no gestionado.)
Mientras lea el código fuente, verá que las llamadas de API simples, como Initialize, sólo llaman a la API de C y devuelven un valor definido como nuestro tipo ReturnCode:
ReturnCode AdpService::Initialize()
{
return (ReturnCode)ADP_Initialize();
}
Cada uno de los métodos de autorización toman un parámetro Guid y, esencialmente, invierten lo que hicimos anteriormente en las propiedades, convirtiendo el GUID en una matriz de bytes gestionada, copiándola en el tipo no gestionado adecuado y pasándola como parámetro a la llamada de API de C.
ReturnCode AdpService::IsAuthorized(Guid applicationId)
{
ADP_APPLICATIONID id;
array<unsigned char>^ guidBytes = applicationId.ToByteArray();
pin_ptr<unsigned char> src = &guidBytes[0];
memcpy(&id, src, 16);
return (ReturnCode)ADP_IsAuthorized(id);
}
Es complicado convertir cadenas de gestionadas a no gestionadas, ya que en C++ no gestionado es posible tener muchos tipos de cadenas distintos. Las cadenas pueden ser de uno o dos bytes, terminan en un valor nulo (habitualmente), pueden ser mutables (const) o no mutables, etc. El lenguaje C++ gestionado de Microsoft incluye métodos para facilitar el cálculo de referencias de cadenas. Al agregar msclr::interop a las referencias de espacio de nombres, podemos utilizar la función marshal_as para convertir datos de cadena tal como hice en la propiedad ApiVersion:
String^ AdpService::ApiVersion::get()
{
return marshal_as<System::String^>(ADP_API_VERSION);
}
Es posible llamar al método marshal_as en un contexto estático (es decir, como función) al convertir desde cadenas nativas a cadenas gestionadas, como se ha indicado anteriormente. Al calcular referencias en cadenas no gestionadas, en primer lugar tiene que crear un contexto de cálculo de referencias en el que calculará las referencias de la cadena3. El cálculo de referencias de datos más exigente para esta API era la función de informe de bloqueos, debido al número y a la complejidad de los parámetros.
ReturnCode AdpService::ReportCrash(
String^ module,
unsigned long lineNumber,
String^ message,
String^ category,
String^ errorData,
array<CrashReportField^>^ customFields )
La llamada de API de C toma estos parámetros y dos más: la longitud de la cadena errorData y el número de elementos en la matriz customFields. Dado que ambos podían determinarse en el contexto de .NET, los evalué dentro de la llamada en vez de forzar al desarrollador a que los incluyese.
Para calcular las referencias de los datos de cadena de las referencias de cadena gestionadas a ‘const wchar_t*’ no gestionadas, tuve que crear un contexto de cálculo de referencias. Las notas contribuidas por la comunidad en la referencia de MSDN sugieren que el contexto de cálculo de referencias mantiene una lista de todos los objetos que gestiona, de manera que bastaba con un contexto para calcular las referencias de todas las cadenas.
marshal_context ctx;
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
ctx.marshal_as<const wchar_t*>(module),
...
Dado que la API de C requiere la longitud de errorData, quise calcularla y pasarla. Aunque sólo pude utilizar el método Length de la cadena gestionada, la "longitud" podía ser de bytes o de caracteres, por lo que eché un vistazo al ejemplo de la documentación. Utilizaba la función de C wcslen (número de caracteres de doble byte). Decidí calcular las referencias de los datos primero y luego enviar la longitud utilizando la misma función.
const wchar_t* eData;
eData = ctx.marshal_as<const wchar_t*>(errorData);
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
...
eData,
wcslen(eData),
…
El parámetro final es una matriz de estructuras para datos de nombre-valor personalizados que pueden incluirse en el informe de bloqueos. Aunque es posible calcular las referencias de una estructura gestionada en una estructura no gestionada, parecía más sencillo crear una clase gestionada como tipo de parámetro y repetirlo en la matriz, convirtiéndola al tipo de API de C antes de pasar los datos. En la firma de método, observe que el parámetro es una referencia a una matriz de referencias a los objetos (¿ve ambos símbolos de intercalación?). Ello se debe a que los objetos son clases gestionadas, a diferencia de las estructuras, que generalmente se pasan por su valor. Dado que no sabemos el número de elementos de la matriz durante la compilación, tendremos que asignarlos durante la ejecución y realizar una limpieza cuando hayamos terminado.
unsigned long fieldCount = 0;
ADP_CrashReportField* fields;
if(customFields != nullptr ) {
fieldCount = Convert::ToUInt32(customFields->Length);
if( fieldCount > 0 ) {
fields = new ADP_CrashReportField[fieldCount];
for( unsigned int i = 0; i < fieldCount; i++ ) {
CrashReportField^ crf = customFields[i];
String^ name = crf->Name;
String^ value = crf->Value;
fields[i].name =
(wchar_t*)ctx.marshal_as<const wchar_t*>(name);
fields[i].value =
(wchar_t*)ctx.marshal_as<const wchar_t*>(value);
}
}
}
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
…
fields,
fieldCount
);
delete[] fields;
Después de crear mis punteros y variables y validar que no voy a generar una excepción de referencia nula, asigno una matriz de estructuras ADP_CrashReportField de la API de C utilizando el nuevo operador. Observe en la última línea que, después de terminar de llamar al método de API, elimino la asignación de matriz.
Por algún motivo, al compilador no le gustaba calcular las referencias de las cadenas de nombre y valor de los objetos CrashReportField en línea, por lo que tuve que copiarlas a referencias de cadena gestionadas. Luego, utilizando el mismo contexto, pude calcular las referencias en una cadena constante nativa.
La API de C enumera los dos campos de la estructura como wchar_t*, sin el modificador ‘const’. Ello implica que la API podría cambiar los datos en esas estructuras. No creo que lo haga y la función marshal_as no admite el cálculo de referencias en una cadena nativa no constante. Por lo tanto, defino los valores de retorno en ‘wchar_t*’. Esto es arriesgado: si la API cambiase los datos en esos punteros, muy probablemente causaría una violación de acceso.
Toques finales
Dado que el ensamblado de .NET se utilizará principalmente en Visual Studio he comentado de forma exhaustiva todas las clases, métodos, propiedades, enumeraciones y todo lo demás, generalmente copiando la documentación utilizada en la publicación SDK C Language Reference. Puede utilizarse el comentario para generar documentación de referencia y también pueden abrirse ventanas emergentes como sugerencias de Intellisense al escribir código.
Como soy una persona puntillosa, fui a los recursos y cambié el número de versión de esta DLL a 0.91.1.* (el asterisco indica al compilador que aumente el número de compilación para que pueda detectar cambios de versión al realizar pruebas), de manera que coincida con la versión 0.91 del archivo lib que estamos empaquetando.
Prueba de la API de .NET
Durante todo este desarrollo tuve que probar las funciones que creaba. Quería asegurarme de que funcionaban desde C#, ya que era el lenguaje que los desarrolladores utilizarían con mayor probabilidad. En el código fuente verá un segundo proyecto que incluye una serie de pruebas de integración que utilizan todas las funciones y propiedades de la clase.
Estas pruebas emplean NUnit como ejecución de pruebas, pero pueden transportarse rápidamente al marco de pruebas de unidad de Visual Studio.
Como deseo probar que las funciones realmente funcionan, son pruebas de integración que envían datos al servicio ATDS y comprueban la respuesta. Debe tener ATDS en funcionamiento para realizar las pruebas. Esto significa que no hay una prueba distinta para comprobar cómo responde la biblioteca de .NET cuando ATDS no funciona, pero la primera prueba devuelve un mensaje de fallo personalizado en ese caso, por lo que puede probar ese escenario en una ejecución distinta.
Los métodos de API siguen la API de C y deben resultarle simples y conocidos si ha leído la Guía del desarrollador de SDK. No obstante, las pruebas constituyen un buen ejemplo de cómo ejercitar cada método y deberían ser útiles si está confuso por alguna de las llamadas.
1 Cálculo de referencias es el proceso de transferir datos de forma segura entre dos sistemas distintos. En este caso, debemos ir con cuidado con la manera como se gestiona la memoria para tipos de puntero o referencia como, por ejemplo, cadenas, matrices y objetos.
2 Cabe la posibilidad de que los ID de ADP cambien en el futuro y ya no coincidan con System.Guid, en cuyo caso esto causaría una interrupción en la API de .NET. No obstante, dado que los GUID son estándar y funcionales, pensé que era una suposición aceptable.
3 Consulte “Visión general del cálculo de referencias en C++” para ver una matriz de tipos y su uso en contexto.