La Web del Programador: Comunidad de Programadores
 
    Pregunta:  15135 - CRAE DLL EN VISUAL C++ PARA ACUCOBOL
Autor:  Diego Muñoz
Hola a todos, tengo que generar una DLL en Visual C++ 6.0, para ser utilizada en un programa en ACUCOBOL.

Este programa, leerá un puerto de la PC, donde se alojara una placa con 16 entradas digitales y 16 salidas digitales.

Yo he hecho algunos programas en Visual C++, pero siempre como CONSOLE APPLICATION.

Les pido que me indiquen como crear una DLL. Yo para probarla haré un programa en Visual Basic 6.0, ya que es lo que mas domino.

La DLL, sera utilizada en ACUCOBOL como dije, pero en un programa que no hago yo sino un tercero.

Muchas gracias.

  Respuesta:  Fernando Gómez
Diego,

híjole, en la que te metiste, mano :D. Es un tanto laborioso, sobre todo si pretendes probarla en VB. Te voy a presentar dos panoramas: tu decides cuál emplear.

PRIMER PANORAMA: UNA DLL NORMALITA.

Primero, generas un proyecto. Luego, agregas tu función de inicio DllMain.

BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}

Quizás quieras saber el por qué fué llamada la librería. Entonces, añades una sentencia switch parecida a la siguiente:

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
// Acciones
break;
case DLL_THREAD_ATTACH:
// Acciones
break;
case DLL_THREAD_DETACH:
// Acciones
break;
case DLL_PROCESS_DETACH:
// Acciones
break;
}

Generalmente, no necesitarás hacer más en esta función, pero dentro de ésta puedes iniciar variables globales, leer datos de disco, etc.

Luego, tendrás que hacer un archivo .DEF, en el que incluirás la declaración de tus funciones a exportar. Si suponemos que exportarás funciones que se llamen Func1, Func2, ..., FuncN, tu archivo DEF lucirá similar a lo que transcribo:

; El punto y coma sirve para hacer comentarios, como el // de C++
LIBRARY "TuLibreria.DLL"

EXPORTS
Func1 @1 PRIVATE
Func2 @2 PRIVATE
; ... demás funciones a exportar ...
FuncN @N PRIVATE

Finalmente, implementas tus funciones Func1, ..., FuncN. Con eso, tienes. Pero, para poderla probar en VB, acuérdate que necesitas accederla a través de la siguiente declaración:

[Public | Private] Declare Sub name Lib "libname" [Alias "aliasname"] [([arglist])]

Acuérdate que el Alias se emplea en caso de que alguna Func1, ..., FuncN tenga nombres cuyos identificadores sean inválidos en VB, pero válidos en C++ (como caracteres _ al inicio del identificador).

Ahora, para que VB reconozca de antemano las funciones de la librería, y para saber cuándo pasar argumentos String, Long, Float, etc, necesitas crear, a tu DLL, un archivo MSIDL (Microsoft Interface Definition Language). Este archivo lo único que hace es describir tus funciones. Puedes consultar el SDK para mayor información sobre sintaxis.

Esto debería bastar para la mayoría de los desarrolladores, pero para tí no. Y esto, porque mencionas que tu librería es para distribución, y que lo hace un tercero. El problema radica en lo siguiente. En el archivo DEF de tu librería, notaste que entre el nombre de tu función y la palabra PRIVATE hay un número ordinal: @1, ..., @N. Estos identificadores sirven para hacer un "mangle". Un mangle es un identificador que contiene un cifrado especial para evitar que se repitan funciones; algo así como un identificador único. El problema es que este mangle no es estándar, por lo que VC++ "manglea" las funciones de manera diferente a Turbo C++ o al compilador de IBM. Por lo tanto, si haces tu DLL en VC++, el desarrollador de ACUCOBOL tendrá que emplear VC++: si emplea Turbo C++, la libreria no le servirá.

Además de éste, hay otros problemas de "preformance", eficiencia en memoria, por lo cuál es inconveniente el empleo de las DLL sencillas, pero el punto anterior es el que más te afectaría. Y aquí es donde entra nuestro segundo panorama.

SEGUNDO PANORAMA: UN COMPONENTE COM (COMPONENT OBJECT MODEL)

Este es el más complicado, porque COM no es una tecnología sencilla. De hecho, no esperes, de antemano, que lo que transcriba abajo resolverá tu problema: es tan sólo una muestra de lo que tendrías que realizar.

El COM se basa en interfaces. En C++, una interfaz es una clase o estructura que contiene puros métodos virtualmente puros (abstractos), esto es, algo así:

class ICualquierInterfaz
{
public:
virtual void Metodo1(/* tus parametros*/) = 0;
};

Por supuesto, puedes emplear cualquier otro valor de retorno en vez de void. Para mayor comodidad, VC++ ha añadido la palabra interface como keyword, pero en realidad está definida como sigue:

#define interface struct

Struct, para que todos sus miembros sean public por default.

Para que una interfaz pueda ser implementada en un componente COM, hay que declarar los miembros como una llamada estándar (pascal):

virtual void __stdcall Metodo1(/* tus parametros*/) = 0;

Generalmente el valor de retorno es HRESULT, que indicará si hubo un error o no:

virtual HRESULT __stdcall Metodo1(/* tus parametros*/) = 0;

De hecho, un Runtime error en VB6 es precísamente una función que regresó un valor diferente de S_OK.

Ya que tienes una pequeña introducción, veamos qué deberías hacer para construir tu componente COM.

Primero, necesitas un Global Unique IDentifier: GUID para tus interfaces, y uno para tu Componente. Estas son de la siguiente forma:
{8C0AF616-CCD2-4c42-9EA5-B17E7AC6DD69}
y que lo genera GUIDGEN.EXE. Microsoft garantiza que el algoritmo empleado para generar este número garantiza, a su cez, que el número sea único.

Segundo, tus interfaces tienen que heredar de IUnknown, una interface que define tres métodos: AddRef, Release y QueryInterface. Las primeras dos funciones sirven para mantener una relación de cuántas instancias de un componente están actualmente cargadas en memoria. La tercera, busca la interfaz que corresponde al GUID (IID) que se pasa como parámetro y regresa, en otro parámetro, un puntero a ésta interfaz (en una nueva instancia).

Tercero, tendrías que exportar estas cuatro funciones:

DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
DllRegisterServer @3 PRIVATE
DllUnregisterServer @4 PRIVATE

DllCanUnloadNow regresa S_OK si el número de instancias cargadas del componente es igual a cero y S_FALSE en caso contrario. Para ello, por supuesto, deberás llevar un control sobre estas variables.

DllRegisterServer registra en el Registry de Windows el GUID. DllUnregisterServer, elimina dicho registro. Esto es de suma importancia, ya que cuando un cliente manda llamar CoCreateInstance para crear el componente, dicha función (que pertenece, claro, al API de COM) busca en el registro que exista, y toma el AppPath de dónde se encuentra la librería DLL que contiene al componente.

De DllGetClassObject hablaremos en unos instantes.

El siguiente paso, es crear una clase que implemente las interfaces requeridas. A su vez, esta clase heredará tambien de IUnknown. Y aquí es donde definirás todos los métodos de las interfaces. Incluso, puedes agregar otros métodos auxiliares, así como variables de clase.

A continuación, necesitas crear una clase que se denomina Creación de factorías. El nombre es muy ad hoc, ya que es esta clase, que hereda de IClassFactory, y ésta a su vez de IUnknown, es la que va a instanciar el componente. Es decir, es la "fábrica que crea componentes". Normalmente, esto sucede en la función CreateInstance, que mandará llamar QueryInterface de la clase del componente. Este método decidirá, de acuerdo a los parámetros que reciba de entrada (el GUID de la interface que solicita el cliente) que QueryInterface llamar. Además, esta clase determinará si soporta Contenimiento y Agregación, que son métodos a través de los cuales COM soporta la "herencia" de componentes. Pero esto no nos incumbe de momento.

Ahora, ya que está todo listo, ¿cómo es que voy a saber qué interfaz me está pidiendo el cliente? ¡Pues claro! Como habrás adivinado, DllGetClassObject es la encargada de esto. Así, esta función funciona (valga la redundancia) como "puente" entre el cliente y el componente. Precísamente en DllGetClassObject, de acuerdo a su parámetro de entrada REFIID (que es un puntero a una estructura GUID que define la GUID de la interfaz buscada) creas un objeto de factoría de clase, en el cuál llamarás a CreateInstance, y que te devolverá el puntero a tu interfaz instanciada, si tiene éxito, claro.

Esto no es todo, por supuesto, pero una vez cubiertos estos pasos ya tienes un componente COM meramente funcional. Aunque hay clases predefinidas para otros aspectos del COM (como los antes mencionados Contenimiento y Agregación), y que también se tiene que implementar ciertos métodos para obtener Marshaling y hacer el componente accesible a través de una red, y que también se recomienda implementar IDispatch para no depender del archivo de definición de las interfaces, esto es lo mínimo que requieres.

Sin embargo, para probarlo desde VB ésto no es suficiente. VB, al igual que otros lenguajes como Java o Delphi, necesita saber la "descripción" de tu componente. Y para ello, necesitará un archivo MSIDL que lo describa.

TERCER PANORAMA: CORBA.

Aunque no te lo enuncié al principio, hay otra opción, pero como verás, es prácticamente igual que la segunda: CORBA. Esta tecnología es "la competencia de COM". Por ello, lo único que cambiaría sería la forma de hacer un componente, no el fondo. Ahora, CORBA tiene la desventaja (o ventaja, depende) de que no es de Microsoft, por lo que su futuro está prácticamente sentenciado. Por eso, cuando mencionaba "componente COM" hacía incapié en que era COM, ya que también existen componentes CORBA, aunque más escasos sobretodo en Windows.

En fin, espero haberte sido de ayuda. Como ves, las dos opciones tienen sus pros y contras. En lo personal, yo emplearía la segunda opción, ya que es más robusta que la primera. Pero dudo que tengas tiempo de aprender COM: es una tecnología complicada de asimilar. Si tienes dudas, contáctame.

Un saludo.
- Est solarus oth mithas -