martes, 7 de julio de 2015

Crear Interfaces COM y usarlas

Todos sabemos lo que es COM (o también conocido como OLE/ActiveX), pero si no lo sabes puedes pasarte por: COM.

Hoy les voy a enseñar a como crear Interfaces COM, pero no con simples marcos ("Interfase Nombre, Metodo1,Metodo2[,Metodo3 ...]") como es hoy en día en la mayoría de los lenguajes de programación, que en realidad ocultan el verdadero diseño de la tecnología COM de Microsoft.
Hoy les voy a crear verdaderas y nativas interfaces para utilizarlas en sus objetos COM.

Este manual esta pensado para aquellos programadores que le gustan los lenguajes basados en prototipos.
¿Clonación de objetos mediante instancias? ¿Quien necesita eso?

Bien, para este manual voy a utilizar el lenguaje de programación Pauscal debido a su simplicidad, su increíble potencia y su alta semejanza al pseudo-código, aunque no lo conozcan, seguro se les va a hacer muy simple entender lo que se trata de realizar, de todas formas voy a tratar de comentar y comentar el código lo mas posible, también les informo que yo uso exclusivamente Windows.

Antes que nada, el manual da por entendido que usted (o ustedes) tiene conocimientos sobre estructuras, tipos de datos, prototipos, clases (POO), punteros y obviamente interfaces (si... con marcos...), si no es así... te recomiendo que empieces a aprender a programar.

También les informo que si ustedes tienen bastos conocimientos sobre los temas recién mencionados, pueden saltar hacia el final del manual, donde habrá un resumen sobre lo que son interfaces COM.

También les dejo una lista de los tipos de datos del lenguaje, por si tienen dudas sobre su alcance.
NombreMemoria requeridaRangoDescripción
Booleano1 byte (8 Bits)Verdad - Falso1 - 0
Byte1 byte (8 Bits)0 - 255Byte sin signo.
ByteSig1 byte (8 Bits)(-128) - 127Byte con signo.
Word2 byte (16 Bits)0 - 65.535Word sin signo.
WordSig2 byte (16 Bits)(-32768) - 32767Word con signo.
Entero4 byte (32 Bits)0 - 4.294.967.295Entero sin signo.
EnteroSig4 byte (32 Bits)(-2.147.483.648) - 2.147.483.647Entero con signo.
Real8 byte (64 Bits)(-1,79769313486232^308) - (-4.94065645841247^-324)Número con coma flotante de doble precisión
Decimal8 byte (64 Bits)(-922.337.203.685.477,5800) - 922.337.203.685.477,5800Número con coma fija de 4 decimales.
CadenaVariable0 ~ 2.000 millones de caracteresCadena caracteres alfanumérica.

Para aprender a crear una interfase, tenemos que conocer una estructura.
Una estructura esta conformada por un identificador, y el identificador de sus miembros. Por ejemplo:

Estruc Estructura, _
            Miembro1:Entero
            Miembro2:Cadena

Estas son general mente declaradas en una variable de tipo (digo "de tipo" cuando el tipo de dato es diferente a los nativos, también puede ser llamado "tipo de dato compuesto" obviamente "compuesto" por diferentes tipos de datos ¿se entiende?). Por ejemplo:

Estruc Estructura, _
            Miembro1:Entero
            Miembro2:Cadena

Var Variable:Estructura

Y se puede acceder a sus miembros (general mente) escribiéndolos antes de un punto. Por ejemplo:

Variable.Miembro ' Hola, soy un comentario.

Para los que conozcan POO (Programación Orientada a Objetos) se les hará conocido, algo que comienza con "P" y termina con "ropiedad", a estas propiedades se les puede escribir y leer valores, lo mismo pasa con un miembro de una estructura.

¿Pero esto como nos ayuda a crear interfaces? Una interfase es en realidad una estructura. Ya me imagino lo que ustedes deben estar pensando.

"Pero yo vi que en C++ una interfase se declara como una clase con métodos virtuales."
"Vos no sabes nada, seguro sos un nerd envidioso! ¿Cuantos programas tenes?"

En C++, es posible declarar una clase con métodos virtuales que es utilizado para crear objetos COM, pero esta clase es en realidad un marco, una mascara del código real, esa clase es en realidad una estructura con prototipos (en caso de C++, puntero a prototipos) y variables (de milagro no puntero a variables...).

Bueno, ya que aclaramos que es una estructura y su semejanza a las clases, aclaremos lo que es un prototipo.
Un prototipo vendría a ser la "cara" de un procedimiento que aún no existe. Un ejemplo de prototipo puede ser:

Prototipo Ejemplo(Argumento1:Entero):Byte

¿Les recuerda algo? Sí, se parece a un procedimiento sin terminar. Un prototipo no tiene código y solo puede ser utilizado si le asignamos una dirección en memoria que contenga código compatible.

¿Sabias que los miembros de una estructura pueden ser de tipo prototipo? Sí, significa que "en teoría" (es una realidad, pero teoría para ustedes) un miembro de una estructura podría ejecutar código funcional sin problema alguno.

Un ejemplo de estructura con miembros de prototipo es el siguiente.

Prototipo Ejemplo(Argumento1:Cadena):Entero

Estruc Estructura,_
          MetodoSimulado:Ejemplo,_
          SimpleMiembro:Entero

Var Variable:Estructura

' Variable.MetodoSimulado("Hola Mundo!") ' Produce error.

Si quisieramos utilizar el método de esta estructura, obtendríamos el típico error de Pauscal (Error al acceder en memoria) esto es debido a que nuestro miembro de prototipo apunta hacia 0, hacia nada.

Para hacer que el método funcione (un miembro de tipo prototipo se dominaría  "método" en una clase normal), debemos hacer que apunte hacia algo, en este caso, cambiare los argumentos del prototipo y haré que apunte hacia la API (IPA en español) MessageBox.

Prototipo Ejemplo(hWnd,lpText,lpCaption,uType:Entero):Entero

Estruc Estructura,_
          MetodoSimulado:Ejemplo,_
          SimpleMiembro:Entero

Var Variable:Estructura

Variable.MetodoSimulado@ = MessageBox@

Si desconocen esos arrobas, significa que estamos estableciendo la dirección de "MetodoSimulado" la dirección de "MessageBox".

Ahora si podemos utilizar el metodo y sera totalmente funcional.

Prototipo Ejemplo(hWnd,lpText,lpCaption,uType:Entero):Entero

Estruc Estructura,_
          MetodoSimulado:Ejemplo,_
          SimpleMiembro:Entero

Var Variable:Estructura
' Utilizamos "CadPtr" para obtener un puntero a la cadena.
Variable.MetodoSimulado(0,CadPtr("Hola!"),CadPtr("Soy un título!"),48)

Este es solo un ejemplo de como podemos utilizar un prototipo en una estructura y ejecutar código 100% funcional, pero cada método y propiedad de un objeto real (creado de una interfase COM) necesita acceder a sus miembros. Para ello, a cada prototipo hay que agregarle un parámetro adicional que se utiliza para obtener el puntero al objeto, este parámetro general mente es el primero.

Prototipo Ejemplo(PtrAEstructura,Parametro1,Parametro2:Entero):Entero

Estruc Estructura,_
          MetodoSimulado:Ejemplo,_
          SimpleMiembro:Entero

Var Variable:Estructura

Vamos a escribir un procedimiento que pueda obtener datos de los miembros de la estructura a través de un puntero a la estructura.

Proc ObtMiembro(PtrEstruc,Param1,Param2:Entero):Entero
   Var @RefAEstruct:Estructura ' Creamos un puntero de referencia a la estructura.
   RefAEstruct@ = PtrEstruc ' Establecemos la dirección a la estructura obtenida del primer parámetro.
   Devolver RefAEstruct.SimpleMiembro ' Devolvemos el miembro "SimpleMiembro" de la estructura.
FinProc

Ya tenemos el prototipo, entonces ¿Como lo usamos? obvio... al igual que con el ejemplo en el que aparece el MessageBox.

Proc ObtMiembro(PtrEstruc,Param1,Param2:Entero):Entero
   Var @RefAEstruct:Estructura ' Creamos un puntero de referencia a la estructura.
   RefAEstruct@ = PtrEstruc ' Establecemos la dirección a la estructura obtenida del primer parámetro.
   Devolver RefAEstruct.SimpleMiembro ' Devolvemos el miembro "SimpleMiembro".
FinProc

Prototipo Ejemplo(PtrAEstructura,Parametro1,Parametro2:Entero):Entero

Estruc Estructura,_
          MetodoSimulado:Ejemplo,_
          SimpleMiembro:Entero

Var Variable:Estructura

Variable.MetodoSimulado@ =  ObtMiembro@ ' Le establecemos al prototipo la dirección del procedimiento.

Variable.SimpleMiembro = 55
' EntCad = Entero a Cadena, Estructura@ = Puntero de la estructura.
Mensaje(EntCad(Variable.MetodoSimulado(Estructura@,0,0) )) ' Los dos parámetros finales no son utilizados.

En el ejemplo, el método "MetodoSimulado" devuelve 55 debido a que obtiene el valor de el miembro "SimpleMiembro" de la estructura establecida en el primer parámetro.

Este es el cuerpo de una interfase, es una estructura con prototipos que aceptan principal mente como primer parámetro un puntero a su mismo objeto.

Pero aún sigue sin ser una interfase funcional, mas adelante veremos porque.

GUID
Un GUID (Identificador único global) es un termino utilizado por Microsoft para un número que su programación para crear una identidad única para una entidad; como un documento de Word. GUID son ampliamente utilizados en productos de Microsoft para identificar interfaces, conjuntos de replicas, registros y otros objetos. Diferentes tipos de objetos tienen diferentes tipos de GUID.

Aparte de ser un termino, un GUID es una estructura que es rellenada con valores aleatorios y que identifican a un objeto.

Estruc GUID,_
Data1:Entero,_
Data2,_
Data3:Word,_
Data4[8]:Byte

Para los que no conozcan los diferentes terminos de C++ sobre GUID, le mostrare una pequeña lista.

  • REFIID - Referencia a estructura GUID.
  • IID - Estructura GUID.
  • CLSID - Estructura GUID.
Referencia: What is GUID

IUnknown
IUnknown es la interface basica de todo objeto, cada objeto creado debe tener como mínimo esta interfase (sus métodos).
Esta interfase consiste en tres métodos.
  • QueryInterface
  • AddRef
  • Release
Al igual que cualquier otra interface, se crea a partir de una estructura con prototipos y cada prototipo 
requiere como primer parámetro el puntero a su objeto.

Para mas información: IUnknown.


CoCreateInstance
Para crear un objeto funcional, debemos obtener la dirección en memoria de tal objeto, y para esto utilizamos la API CoCreateInstance.

CoCreateInstance tiene la siguiente sintaxis (en  C++).
HRESULT CoCreateInstance(
  _In_  REFCLSID  rclsid,
  _In_  LPUNKNOWN pUnkOuter,
  _In_  DWORD     dwClsContext,
  _In_  REFIID    riid,
  _Out_ LPVOID    *ppv
);
Aca podemos ver las innecesaria re-definiciones de los tipos de datos elementales del lenguaje.
En Pauscal, esto es:

Proc CoCreateInstance(Referencia rclsid:GUID,_
                                     pUnkOuter:Entero,_
                                     dwClsContext:Entero,_
                                     Referencia riid:GUID,_
                                     ppv:Entero):Entero, "Ole32.dll"

Como se puede apreciar, REFCLSID y REFIID son referencias a la misma estructura (GUID), pero con diferente nombre (aunque el nombre es irrelevante, lo que cuenta son sus miembros).

A continuación, una descripción de los parámetros:
  1. rclsid - El CLSID (GUID) asociado con los datos y el código que se utilizarán para crear el objeto.
  2. pUnkOuter - Indica que el objeto esta siendo creado como parte de un agregado.
  3. dwClsConext - Contexto en el que el código se gestiona
  4. riid - Una referencia al identificador de la interfaz que se utiliza para comunicarse con el objeto.
  5. ppv - Dirección de la variable que recibe el puntero de la interfase solicitada en riid.
El parametro 1 y 5 son los que realmente nos importa.

rclsid es el identificador de el objeto, no voy a entrar en como obtenerlo ya que este manual no enseña a utilizar llanamente la tecnología COM, solo voy a enseñar a crear Interfaces COM.

El parámetro 5 es un puntero hacia una variable que contendrá el puntero. El puntero obtenido es un puntero hacia un puntero VTable (no un puntero hacia un puntero nulo como se dice en C++, eso no existe).

¿Recuerdan cuando les dije que ustedes no habían aprendido a crear una interfase funcional?
Me refería a esto.

Ustedes aprendieron a crear VTables (Estructuras miembros de tipo prototipo). Una ejemplo-referencia de interfase es la siguiente.

Prototipo SoyUnPrototipo(PtrEstructura:Entero,Opcional Param1,Param2:Entero):Entero

Estruc Estructura,_
           Metodo1:SoyUnPrototipo

Estruc Interface,@VTable:Estructura

Una interfase real es una estructura que tiene como miembro un puntero a una VTable. Ahora les voy a mostrar un ejemplo real de interfase 100% real.

Prototipo pQueryInterface(PtrEstruc:Entero,riid:GUID,ptrReturn:Entero):Entero
Prototipo pAddRef(PtrEstruc:Entero):Entero
Prototipo pRelease(PtrEstruc:Entero):Entero
Prototipo pHrInit(PtrEstruc:Entero):Entero
Prototipo pAddTab(PtrEstruc,hWnd:Entero):Entero
Prototipo pDeleteTab(PtrEstruc,hWnd:Entero):Entero
Prototipo pActivateTab(PtrEstruc,hWnd:Entero):Entero
Prototipo pSetActivateAlt(PtrEstruc,hWnd:Entero):Entero

Estruc pITaskbarList,_
           QueryInterface:pQueryInterface,_
           AddRef:pAddRef,_
           Release:pRelease,_
           HrInit:pHrInit,_
           AddTab:pAddTab,_
           DeleteTab:pDeleteTab,_
           ActivateTab:pActivateTab,_
           SetActivateAlt:pSetActivateAlt

Estruc ITaskbarList,@Ptr:pITaskbarList

Como interfase, debe contener como mínimo la interface IUnknown incorporada en sus primeros tres miembros, de otra forma, no seria valida y no la podriamos usar.

La CLSID de ITaskbarList es "{56FDF344-FD6D-11D0-958A-006097C9A090}", pueden convertir esta cadena a su correspondiente GUID estructura con la IPA CLSIDFromString.

Ahora vamos a utilizar esta Interface (POR FIN!). Intenten seguir el siguiente código, despues se los explicare con detalle, les explico las funciones que voy a importar de la libreria "COM.prp".

  • CadGUID - Convierte una cadena GUID valida en un GUID estructura.

Importar "COM.prp"

' Constantes
Const CLSCTX_INPROC_SERVER = &1
Const CLSCTX_INPROC_HANDLER = &2
Const CLSCTX_LOCAL_SERVER = &4
Const CLSCTX_REMOTE_SERVER = &10
Const CLSCTX_ALL = CLSCTX_INPROC_SERVER Or CLSCTX_INPROC_HANDLER Or CLSCTX_LOCAL_SERVER Or CLSCTX_REMOTE_SERVER

Var PtrObtener:Entero,@TaskbarList:ITaskbarList
CoInitialize ' Inicializamos COM.
CoCreateInstance(_
CadGUID("{56FDF344-FD6D-11D0-958A-006097C9A090}"),_ ' GUID del objeto a obtener.
0,_ ' Este parametro es innecesario, se establece en nulo.
CLSCTX_ALL,_ ' Solamente pongan CLSCTX_ALL...
CadGUID("{00000000-0000-0000-C000-000000000046}"),_ ' Este parametro es innecesario, pueden poner el IID de la interface IUnknown, pero generalmente cada objeto tiene su IID.
PtrObtener@) ' Puntero a la variable de tipo entero.

Si PtrObtener <> 0 Entonces TaskbarList@ = PtrObtener
CoUninitialize ' Finalizamos COM.

Primero, declaramos las constantes necesarias, creamos 2 variables. una de tipo entero (PtrObtener) que obtendra el puntero del objeto, y un puntero a la interface creada que podremos utilizar una vez le asignemos la dirección devuelta a PtrObtener por CoCreateInstance.

Llamamos a la IPA, su primer parametro es una estructura GUID, utilizamos CadGUID para convertir la cadena a una estructura GUID, el segundo parametro es innecesario, lo establecemos nulo, el tercer parametro indica el contexto del objeto, es preferible establecer CLSCTX_ALL, el cuarto parametro es el IID del objeto, podemos establecer el IID de la interfase IUnknown, el quinto parametro es el puntero a la variable que obtendra la dirección del objeto (este objeto contiene aparte un puntero a la VTable), establecimos la dirección de PtrObtener.

Una vez llamada la función (y suponiendo que no haya ocurrido ningún error) la IPA retornara 0, PtrObtener contendra el puntero al objeto y se lo asignamos a TaskbarList.

Una vez hecho esto, el objeto esta creado y puede ser utilizado, pero recuerden establecer la dirección del objeto en el primer parametro de los metodos.

Aunque ya lo explique muchas veces, seguro no recuerdan que en cada metodo hay que establecer el puntero al objeto; asique les voy a mostrar como usar un objeto.

Taskbarlist.Ptr.HrInit(TaskbarList@)
Taskbarlist.Ptr.DeleteTab(TaskbarList@,Formulario.hWnd) ' Borra el icono de un formulario en la barra de tareas.

No fue tan dificil ¿Verdad?