645 lines
16 KiB
C
645 lines
16 KiB
C
/****************************** Module Header ******************************\
|
|
* Module Name: DMGMEM.C
|
|
*
|
|
* DDE Manager memory management functions.
|
|
*
|
|
* Created: 5/31/90 Rich Gartland
|
|
*
|
|
* This module contains routines which mimic memory management functions
|
|
* used by the OS/2 version of the DDEMGR library. Some are emulations
|
|
* of OS/2 calls, and others emulate DDEMGR macros built on OS/2 calls.
|
|
* Old function new function
|
|
* --------------------------------------
|
|
* WinCreateHeap DmgCreateHeap
|
|
* WinDestroyHeap DmgDestroyHeap
|
|
* FarAllocMem FarAllocMem
|
|
* FarFreeMem FarFreeMem
|
|
*
|
|
* Copyright (c) 1990, Aldus Corporation
|
|
\***************************************************************************/
|
|
|
|
#include "ddemlp.h"
|
|
#include <memory.h>
|
|
|
|
#ifdef DEBUG
|
|
|
|
#define GML_FREE 1
|
|
#define GML_ALLOC 2
|
|
#define MAX_CLOGS 500
|
|
#define STKTRACE_LEN 3
|
|
|
|
typedef struct _GMLOG {
|
|
HGLOBAL h;
|
|
WORD flags; // GML_
|
|
WORD msg;
|
|
WORD cLocks;
|
|
WORD stktrace[STKTRACE_LEN];
|
|
WORD stktracePrev[STKTRACE_LEN];
|
|
} GMLOG, far * LPGMLOG;
|
|
|
|
GMLOG gmlog[MAX_CLOGS];
|
|
WORD cGmLogs = 0;
|
|
int TraceApiLevel = 0;
|
|
|
|
|
|
VOID TraceApiIn(
|
|
LPSTR psz)
|
|
{
|
|
char szT[10];
|
|
|
|
wsprintf(szT, "%2d | ", TraceApiLevel);
|
|
TraceApiLevel++;
|
|
OutputDebugString(szT);
|
|
OutputDebugString(psz);
|
|
if (bDbgFlags & DBF_STOPONTRACE) {
|
|
DebugBreak();
|
|
}
|
|
}
|
|
|
|
VOID TraceApiOut(
|
|
LPSTR psz)
|
|
{
|
|
char szT[10];
|
|
|
|
TraceApiLevel--;
|
|
wsprintf(szT, "%2d | ", TraceApiLevel);
|
|
OutputDebugString(szT);
|
|
OutputDebugString(psz);
|
|
if (bDbgFlags & DBF_STOPONTRACE) {
|
|
DebugBreak();
|
|
}
|
|
}
|
|
|
|
VOID DumpGmObject(
|
|
LPGMLOG pgmlog)
|
|
{
|
|
char szT[100];
|
|
|
|
wsprintf(szT,
|
|
"\n\rh=%4x flags=%4x msg=%4x stacks:\n\r%04x %04x %04x %04x %04x\n\r%04x %04x %04x %04x %04x",
|
|
pgmlog->h,
|
|
pgmlog->flags,
|
|
pgmlog->msg,
|
|
pgmlog->stktrace[0],
|
|
pgmlog->stktrace[1],
|
|
pgmlog->stktrace[2],
|
|
pgmlog->stktrace[3],
|
|
pgmlog->stktrace[4],
|
|
pgmlog->stktracePrev[0],
|
|
pgmlog->stktracePrev[1],
|
|
pgmlog->stktracePrev[2],
|
|
pgmlog->stktracePrev[3],
|
|
pgmlog->stktracePrev[4]
|
|
);
|
|
OutputDebugString(szT);
|
|
}
|
|
|
|
|
|
HGLOBAL LogGlobalReAlloc(
|
|
HGLOBAL h,
|
|
DWORD cb,
|
|
UINT flags)
|
|
{
|
|
HGLOBAL hRet;
|
|
WORD i;
|
|
|
|
hRet = GlobalReAlloc(h, cb, flags);
|
|
if (bDbgFlags & DBF_LOGALLOCS && h != hRet) {
|
|
if (hRet != NULL) {
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (h & 0xFFFE)) {
|
|
gmlog[i].flags = GML_FREE;
|
|
hmemcpy(gmlog[i].stktracePrev, gmlog[i].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[i].stktrace);
|
|
}
|
|
if ((gmlog[i].h & 0xFFFE) == (hRet & 0xFFFE)) {
|
|
gmlog[i].flags = GML_ALLOC;
|
|
hmemcpy(gmlog[i].stktracePrev, gmlog[i].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[i].stktrace);
|
|
return(hRet);
|
|
}
|
|
}
|
|
if (cGmLogs >= MAX_CLOGS) {
|
|
OutputDebugString("\n\rGlobal logging table overflow.");
|
|
DumpGlobalLogs();
|
|
DebugBreak();
|
|
return(hRet);
|
|
}
|
|
|
|
gmlog[cGmLogs].flags = GML_ALLOC;
|
|
gmlog[cGmLogs].msg = 0;
|
|
gmlog[cGmLogs].h = hRet;
|
|
gmlog[cGmLogs].cLocks = 0;
|
|
hmemcpy(gmlog[cGmLogs].stktracePrev, gmlog[cGmLogs].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[cGmLogs].stktrace);
|
|
cGmLogs++;
|
|
}
|
|
}
|
|
return(hRet);
|
|
}
|
|
|
|
|
|
|
|
HGLOBAL LogGlobalAlloc(
|
|
UINT flags,
|
|
DWORD cb)
|
|
{
|
|
HGLOBAL hRet;
|
|
WORD i;
|
|
|
|
hRet = GlobalAlloc(flags, cb);
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
if (hRet != NULL) {
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (hRet & 0xFFFE)) {
|
|
gmlog[i].flags = GML_ALLOC;
|
|
hmemcpy(gmlog[i].stktracePrev, gmlog[i].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[i].stktrace);
|
|
return(hRet);
|
|
}
|
|
}
|
|
if (cGmLogs >= MAX_CLOGS) {
|
|
OutputDebugString("\n\rGlobal logging table overflow.");
|
|
DumpGlobalLogs();
|
|
DebugBreak();
|
|
return(hRet);
|
|
}
|
|
|
|
gmlog[cGmLogs].flags = GML_ALLOC;
|
|
gmlog[cGmLogs].msg = 0;
|
|
gmlog[cGmLogs].h = hRet;
|
|
gmlog[cGmLogs].cLocks = 0;
|
|
hmemcpy(gmlog[cGmLogs].stktracePrev, gmlog[cGmLogs].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[cGmLogs].stktrace);
|
|
cGmLogs++;
|
|
}
|
|
}
|
|
return(hRet);
|
|
}
|
|
|
|
|
|
void FAR * LogGlobalLock(
|
|
HGLOBAL h)
|
|
{
|
|
WORD i;
|
|
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (h & 0xFFFE)) {
|
|
break;
|
|
}
|
|
}
|
|
if (i < cGmLogs) {
|
|
gmlog[i].cLocks++;
|
|
if (gmlog[i].flags == GML_FREE) {
|
|
DumpGmObject(&gmlog[i]);
|
|
OutputDebugString("\n\rGlobalLock will fail.");
|
|
DebugBreak();
|
|
}
|
|
}
|
|
}
|
|
return(GlobalLock(h));
|
|
}
|
|
|
|
|
|
BOOL LogGlobalUnlock(
|
|
HGLOBAL h)
|
|
{
|
|
WORD i;
|
|
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (h & 0xFFFE)) {
|
|
break;
|
|
}
|
|
}
|
|
if (i < cGmLogs) {
|
|
if (gmlog[i].cLocks == 0 || gmlog[i].flags == GML_FREE) {
|
|
DumpGmObject(&gmlog[i]);
|
|
OutputDebugString("\n\rGlobalUnlock will fail.");
|
|
DebugBreak();
|
|
}
|
|
gmlog[i].cLocks--;
|
|
}
|
|
}
|
|
return(GlobalUnlock(h));
|
|
}
|
|
|
|
|
|
HGLOBAL LogGlobalFree(
|
|
HGLOBAL h)
|
|
{
|
|
WORD i;
|
|
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (h & 0xFFFE)) {
|
|
if (gmlog[i].flags == GML_FREE) {
|
|
DumpGmObject(&gmlog[i]);
|
|
OutputDebugString("\n\rFreeing free object.\n\r");
|
|
DebugBreak();
|
|
}
|
|
gmlog[i].flags = GML_FREE;
|
|
hmemcpy(gmlog[i].stktracePrev, gmlog[i].stktrace,
|
|
sizeof(WORD) * STKTRACE_LEN);
|
|
StkTrace(STKTRACE_LEN, gmlog[i].stktrace);
|
|
return(GlobalFree(h));
|
|
}
|
|
}
|
|
OutputDebugString("\n\rGlobal object being freed not found in logs.");
|
|
DebugBreak();
|
|
}
|
|
return(GlobalFree(h));
|
|
}
|
|
|
|
|
|
VOID LogDdeObject(
|
|
UINT msg,
|
|
LONG lParam)
|
|
{
|
|
HGLOBAL h;
|
|
WORD i;
|
|
char szT[100];
|
|
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
switch (msg & 0x0FFF) {
|
|
case WM_DDE_DATA:
|
|
case WM_DDE_POKE:
|
|
case WM_DDE_ADVISE:
|
|
case 0:
|
|
h = LOWORD(lParam);
|
|
break;
|
|
|
|
case WM_DDE_EXECUTE:
|
|
h = HIWORD(lParam);
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
if (h == 0) {
|
|
return;
|
|
}
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if ((gmlog[i].h & 0xFFFE) == (h & 0xFFFE)) {
|
|
if (gmlog[i].flags == GML_FREE) {
|
|
DumpGmObject(&gmlog[i]);
|
|
wsprintf(szT, "\n\rLogging free DDE Object! [%4x]\n\r", msg);
|
|
OutputDebugString(szT);
|
|
DebugBreak();
|
|
}
|
|
if (msg & 0xFFF) {
|
|
gmlog[i].msg = msg;
|
|
} else {
|
|
gmlog[i].msg = (gmlog[i].msg & 0x0FFF) | msg;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
VOID DumpGlobalLogs()
|
|
{
|
|
WORD i;
|
|
char szT[100];
|
|
|
|
if (bDbgFlags & DBF_LOGALLOCS) {
|
|
wsprintf(szT, "\n\rDumpGlobalLogs - cGmLogs = %d", cGmLogs);
|
|
OutputDebugString(szT);
|
|
for (i = 0; i < cGmLogs; i++) {
|
|
if (gmlog[i].flags == GML_ALLOC) {
|
|
DumpGmObject(&gmlog[i]);
|
|
}
|
|
}
|
|
wsprintf(szT, "\n\rDDEML CS=%04x\n\r", HIWORD((LPVOID)DumpGlobalLogs));
|
|
OutputDebugString(szT);
|
|
}
|
|
}
|
|
|
|
#endif // DEBUG
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Creates a new heap and returns a handle to it.
|
|
* Returns NULL on error.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 5/31/90 Rich Gartland
|
|
\***************************************************************************/
|
|
HANDLE DmgCreateHeap(wSize)
|
|
WORD wSize;
|
|
{
|
|
HANDLE hMem;
|
|
DWORD dwSize;
|
|
|
|
dwSize = wSize;
|
|
/* Allocate the memory from a global data segment */
|
|
if (!(hMem = GLOBALALLOC(GMEM_MOVEABLE, dwSize)))
|
|
return(NULL);
|
|
|
|
/* use LocalInit to establish heap mgmt structures in the seg */
|
|
if (!LocalInit(hMem, NULL, wSize - 1)) {
|
|
GLOBALFREE(hMem);
|
|
return(NULL);
|
|
}
|
|
|
|
return(hMem);
|
|
}
|
|
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Destroys a heap previously created with DmgCreateHeap.
|
|
* Returns nonzero on error.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 5/31/90 Rich Gartland
|
|
\***************************************************************************/
|
|
HANDLE DmgDestroyHeap(hheap)
|
|
HANDLE hheap;
|
|
{
|
|
/* now free the block and return the result (NULL if success) */
|
|
return(GLOBALFREE(hheap));
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Attempts to recover from memory allocation errors.
|
|
*
|
|
* Returns fRetry - ok to attempt reallocation.
|
|
*/
|
|
BOOL ProcessMemError(
|
|
HANDLE hheap)
|
|
{
|
|
PAPPINFO pai;
|
|
|
|
// first locate what instance this heap is assocaited with
|
|
|
|
SEMENTER();
|
|
pai = pAppInfoList;
|
|
while (pai && pai->hheapApp != hheap) {
|
|
pai = pai->next;
|
|
}
|
|
if (!pai) {
|
|
SEMLEAVE();
|
|
return(FALSE); // not associated with an instance, no recourse.
|
|
}
|
|
|
|
/*
|
|
* Free our emergency reserve memory and post a message to our master
|
|
* window to handle heap cleanup.
|
|
*/
|
|
if (pai->lpMemReserve) {
|
|
FarFreeMem(pai->lpMemReserve);
|
|
pai->lpMemReserve = NULL;
|
|
MONERROR(pai, DMLERR_LOW_MEMORY);
|
|
DoCallback(pai, NULL, 0, 0, 0, XTYP_ERROR, NULL, DMLERR_LOW_MEMORY, 0L);
|
|
SEMLEAVE();
|
|
if (!PostMessage(pai->hwndDmg, UM_FIXHEAP, 0, (LONG)(LPSTR)pai)) {
|
|
SETLASTERROR(pai, DMLERR_SYS_ERROR);
|
|
return(FALSE);
|
|
}
|
|
return(TRUE);
|
|
}
|
|
|
|
return(FALSE); // no reserve memory, were dead bud.
|
|
}
|
|
|
|
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Allocates a new block of a given size from a heap.
|
|
* Returns NULL on error, far pointer to the block otherwise.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 5/31/90 Rich Gartland
|
|
\***************************************************************************/
|
|
|
|
LPVOID FarAllocMem(hheap, wSize)
|
|
HANDLE hheap;
|
|
WORD wSize;
|
|
{
|
|
|
|
LPSTR lpMem;
|
|
PSTR pMem;
|
|
WORD wSaveDS;
|
|
|
|
/* lock the handle to get a far pointer */
|
|
lpMem = (LPSTR)GLOBALPTR(hheap);
|
|
if (!lpMem)
|
|
return(NULL);
|
|
|
|
do {
|
|
/* Do some magic here using the segment selector, to switch our
|
|
* ds to the heap's segment. Then, our LocalAlloc will work fine.
|
|
*/
|
|
wSaveDS = SwitchDS(HIWORD(lpMem));
|
|
|
|
/* Allocate the block */
|
|
// Note: if you remove the LMEM_FIXED flag you will break the handle
|
|
// validation in DdeFreeDataHandle & get a big handle leak!!
|
|
pMem = (PSTR)LocalAlloc((WORD)LPTR, wSize); // LPTR = fixed | zeroinit
|
|
|
|
SwitchDS(wSaveDS);
|
|
} while (pMem == NULL && ProcessMemError(hheap));
|
|
|
|
#ifdef WATCHHEAPS
|
|
if (pMem) {
|
|
LogAlloc((DWORD)MAKELONG(pMem, HIWORD(lpMem)), wSize,
|
|
RGB(0xff, 0, 0), hInstance);
|
|
}
|
|
#endif
|
|
/* set up the far return value, based on the success of LocalAlloc */
|
|
return (LPSTR)(pMem ? MAKELONG(pMem, HIWORD(lpMem)) : NULL);
|
|
}
|
|
|
|
|
|
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Frees a block of a given size from a heap.
|
|
* Returns NULL on success, far pointer to the block otherwise.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 5/31/90 Rich Gartland
|
|
\***************************************************************************/
|
|
|
|
void FarFreeMem(
|
|
LPVOID lpMem)
|
|
{
|
|
|
|
WORD wSaveDS;
|
|
#ifdef WATCHHEAPS
|
|
WORD sz;
|
|
#endif
|
|
|
|
/* Do some magic here using the segment selector, to switch our
|
|
* ds to the heap's segment. Then, our LocalFree will work fine.
|
|
*/
|
|
wSaveDS = SwitchDS(HIWORD(lpMem));
|
|
#ifdef WATCHHEAPS
|
|
sz = LocalSize((LOWORD((DWORD)lpMem)));
|
|
#endif
|
|
/* Free the block */
|
|
LocalFree(LocalHandle(LOWORD((DWORD)lpMem)));
|
|
|
|
SwitchDS(wSaveDS);
|
|
#ifdef WATCHHEAPS
|
|
LogAlloc((DWORD)lpMem, sz, RGB(0x80, 0x80, 0x80), hInstance);
|
|
#endif
|
|
}
|
|
|
|
|
|
int FAR PASCAL WEP (int);
|
|
int FAR PASCAL LibMain(HANDLE, WORD, WORD, LPCSTR);
|
|
#pragma alloc_text(INIT_TEXT,LibMain,WEP)
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Does some initialization for the DLL. Called from LibEntry.asm
|
|
* Returns 1 on success, 0 otherwise.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 6/5/90 Rich Gartland
|
|
\***************************************************************************/
|
|
|
|
int FAR PASCAL LibMain (hI, wDS, cbHS, lpszCL)
|
|
HANDLE hI; /* instance handle */
|
|
WORD wDS; /* data segment */
|
|
WORD cbHS; /* heapsize */
|
|
LPCSTR lpszCL; /* command line */
|
|
{
|
|
extern ATOM gatomDDEMLMom;
|
|
extern ATOM gatomDMGClass;
|
|
|
|
|
|
#if 0
|
|
/* We won't unlock the data segment, as typically happens here */
|
|
|
|
/* Init the semaphore -- probably just a stub now */
|
|
SEMINIT();
|
|
#endif
|
|
/* set up the global instance handle variable */
|
|
hInstance = hI;
|
|
|
|
/* set up class atoms. Note we use RegisterWindowMessage because
|
|
* it comes from the same user atom table used for class atoms.
|
|
*/
|
|
gatomDDEMLMom = RegisterWindowMessage("DDEMLMom");
|
|
gatomDMGClass = RegisterWindowMessage("DMGClass");
|
|
|
|
return(1);
|
|
|
|
}
|
|
|
|
|
|
VOID RegisterClasses()
|
|
{
|
|
WNDCLASS cls;
|
|
|
|
cls.hIcon = NULL;
|
|
cls.hCursor = NULL;
|
|
cls.lpszMenuName = NULL;
|
|
cls.hbrBackground = NULL;
|
|
cls.style = 0; // CS_GLOBALCLASS
|
|
cls.hInstance = hInstance;
|
|
cls.cbClsExtra = 0;
|
|
|
|
cls.cbWndExtra = sizeof(VOID FAR *) + sizeof(WORD);
|
|
cls.lpszClassName = SZCLIENTCLASS;
|
|
cls.lpfnWndProc = (WNDPROC)ClientWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
// cls.cbWndExtra = sizeof(VOID FAR *) + sizeof(WORD);
|
|
cls.lpszClassName = SZSERVERCLASS;
|
|
cls.lpfnWndProc = (WNDPROC)ServerWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
// cls.cbWndExtra = sizeof(VOID FAR *) + sizeof(WORD);
|
|
cls.lpszClassName = SZDMGCLASS;
|
|
cls.lpfnWndProc = (WNDPROC)DmgWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
cls.cbWndExtra = sizeof(VOID FAR *) + sizeof(WORD) + sizeof(WORD);
|
|
cls.lpszClassName = SZCONVLISTCLASS;
|
|
cls.lpfnWndProc = (WNDPROC)ConvListWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
cls.cbWndExtra = sizeof(VOID FAR *);
|
|
cls.lpszClassName = SZMONITORCLASS;
|
|
cls.lpfnWndProc = (WNDPROC)MonitorWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
cls.cbWndExtra = sizeof(VOID FAR *);
|
|
cls.lpszClassName = SZFRAMECLASS;
|
|
cls.lpfnWndProc = (WNDPROC)subframeWndProc;
|
|
RegisterClass(&cls);
|
|
|
|
#ifdef WATCHHEAPS
|
|
cls.cbWndExtra = 0;
|
|
cls.lpszClassName = SZHEAPWATCHCLASS;
|
|
cls.lpfnWndProc = DefWindowProc;
|
|
cls.hCursor = LoadCursor(NULL, IDC_ARROW);
|
|
cls.hbrBackground = GetStockObject(WHITE_BRUSH);
|
|
RegisterClass(&cls);
|
|
#endif // WATCHHEAPS
|
|
}
|
|
|
|
|
|
#if 0
|
|
VOID UnregisterClasses()
|
|
{
|
|
UnregisterClass(SZCLIENTCLASS, hInstance);
|
|
UnregisterClass(SZSERVERCLASS, hInstance);
|
|
UnregisterClass(SZDMGCLASS, hInstance);
|
|
UnregisterClass(SZCONVLISTCLASS, hInstance);
|
|
UnregisterClass(SZMONITORCLASS, hInstance);
|
|
UnregisterClass(SZFRAMECLASS, hInstance);
|
|
#ifdef WATCHHEAPS
|
|
UnregisterClass(SZHEAPWATCHCLASS, hInstance);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
|
|
/***************************** Private Function ****************************\
|
|
*
|
|
* Does the termination for the DLL.
|
|
* Returns 1 on success, 0 otherwise.
|
|
*
|
|
*
|
|
* History:
|
|
* Created 6/5/90 Rich Gartland
|
|
\***************************************************************************/
|
|
|
|
int FAR PASCAL WEP (nParameter)
|
|
int nParameter;
|
|
{
|
|
|
|
if (nParameter == WEP_SYSTEM_EXIT) {
|
|
/* DdeUninitialize(); */
|
|
return(1);
|
|
} else {
|
|
if (nParameter == WEP_FREE_DLL) {
|
|
/* DdeUninitialize(); */
|
|
return(1);
|
|
}
|
|
}
|
|
|
|
}
|