Added two things:
[charm.git] / src / libs / ck-libs / tcharm / tcharm.C
1 /*
2 Threaded Charm++ "Framework Framework"
3
4 Orion Sky Lawlor, olawlor@acm.org, 11/19/2001
5  */
6 #include "tcharm.h"
7 #include <ctype.h>
8
9 #if 0
10     /*Many debugging statements:*/
11 #    define DBG(x) ckout<<"["<<thisIndex<<"] TCHARM> "<<x<<endl;
12 #    define DBGX(x) ckout<<"PE("<<CkMyPe()<<") TCHARM> "<<x<<endl;
13 #else
14     /*No debugging statements*/
15 #    define DBG(x) /*empty*/
16 #    define DBGX(x) /*empty*/
17 #endif
18
19 CtvDeclare(TCharm *,_curTCharm);
20 CpvDeclare(inState,_stateTCharm);
21
22 static int lastNumChunks=0;
23 /*readonly*/ int tcharm_nomig=0, tcharm_nothreads=0;
24
25 void TCharm::nodeInit(void)
26 {
27   CtvInitialize(TCharm *,_curTCharm);
28   CtvAccess(_curTCharm)=NULL;
29   CpvInitialize(inState,_stateTCharm);
30   TCharm::setState(inNodeSetup);
31   TCharmUserNodeSetup();
32   FTN_NAME(TCHARM_USER_NODE_SETUP,tcharm_user_node_setup)();
33   TCharm::setState(inInit);
34 }
35
36 class TCharmTraceLibList {
37         enum {maxLibs=20,maxLibNameLen=15};
38         //List of libraries we want to trace:
39         int curLibs;
40         char libNames[maxLibs][maxLibNameLen];
41 public:
42         TCharmTraceLibList() {curLibs=0;}
43         void addTracing(const char *lib) 
44         { //We want to trace this library-- add its name to the list.
45                 CkPrintf("TCHARM> Will trace calls to library %s\n",lib);
46                 int i;
47                 for (i=0;0!=*lib;i++,lib++)
48                         libNames[curLibs][i]=tolower(*lib);
49                 libNames[curLibs][i]=0;
50                 curLibs++;
51         }
52         int isTracing(const char *lib) const {
53                 for (int i=0;i<curLibs;i++) 
54                         if (0==strcmp(lib,libNames[i]))
55                                 return 1;
56                 return 0;
57         }
58 };
59 TCharmTraceLibList tcharm_tracelibs;
60
61 void TCharmApiTrace(const char *routineName,const char *libraryName)
62 {
63         if (!tcharm_tracelibs.isTracing(libraryName)) return;
64         TCharm *tc=CtvAccess(_curTCharm);
65         char where[100];
66         if (tc==NULL) sprintf(where,"[serial context on %d]",CkMyPe());
67         else sprintf(where,"[vp %d, p %d]",tc->getElement(),CkMyPe());
68         CmiPrintf("%s Called routine %s\n",where,routineName);
69 }
70
71 static void startTCharmThread(TCharmInitMsg *msg)
72 {
73         TCharm::setState(inDriver);
74         CtvAccess(_curTCharm)->activateHeap();
75         typedef void (*threadFn_t)(void *);
76         ((threadFn_t)msg->threadFn)(msg->data);
77         CtvAccess(_curTCharm)->done();
78 }
79
80 TCharm::TCharm(TCharmInitMsg *initMsg_)
81 {
82   initMsg=initMsg_;
83   if (tcharm_nothreads) 
84   { //Don't even make a new thread-- just use main thread
85     tid=CthSelf();
86   } 
87   else /*Create a thread normally*/
88   {
89     if (tcharm_nomig) /*Nonmigratable version, for debugging*/ 
90       tid=CthCreate((CthVoidFn)startTCharmThread,initMsg,initMsg->stackSize);
91     else
92       tid=CthCreateMigratable((CthVoidFn)startTCharmThread,initMsg,initMsg->stackSize);
93   }
94   CtvAccessOther(tid,_curTCharm)=this;
95   TCharm::setState(inInit);
96   isStopped=true;
97   threadInfo.tProxy=CProxy_TCharm(thisArrayID);
98   threadInfo.thisElement=thisIndex;
99   threadInfo.numElements=initMsg->numElements;
100   heapBlocks=CmiIsomallocBlockListNew();
101   nUd=0;
102   usesAtSync=CmiTrue;
103   ready();
104 }
105
106 TCharm::TCharm(CkMigrateMessage *msg)
107         :ArrayElement1D(msg)
108 {
109   initMsg=NULL;
110   tid=NULL;
111   threadInfo.tProxy=CProxy_TCharm(thisArrayID);  
112 }
113
114 void TCharm::pup(PUP::er &p) {
115 //Pup superclass
116   ArrayElement1D::pup(p);  
117
118   p(isStopped);
119   p(threadInfo.thisElement);
120   p(threadInfo.numElements);
121
122 #ifndef CMK_OPTIMIZE
123   DBG("Packing thread");
124   if (!isStopped)
125     CkAbort("Cannot pup a running thread.  You must suspend before migrating.\n");
126   if (tcharm_nomig) CkAbort("Cannot migrate with the +tcharm_nomig option!\n");
127 #endif
128
129 //Pup thread (EVIL & UGLY):
130   //This seekBlock allows us to reorder the packing/unpacking--
131   // This is needed because the userData depends on the thread's stack
132   // and heap data both at pack and unpack time.
133   PUP::seekBlock s(p,2);
134   if (p.isUnpacking()) 
135   {//In this case, unpack the thread & heap before the user data
136     s.seek(1);
137     tid = CthPup((pup_er) &p, tid);
138     CtvAccessOther(tid,_curTCharm)=this;
139     CmiIsomallocBlockListPup((pup_er) &p,&heapBlocks);
140   }
141   
142   //Pack all user data
143   TCharm::setState(inPup);
144   s.seek(0);
145   p(nUd);
146   for(int i=0;i<nUd;i++) 
147     ud[i].pup(p);
148   TCharm::setState(inFramework);
149
150   if (!p.isUnpacking()) 
151   {//In this case, pack the thread after the user data
152     s.seek(1);
153     tid = CthPup((pup_er) &p, tid);
154     CmiIsomallocBlockListPup((pup_er) &p,&heapBlocks);
155   }
156   s.endBlock(); //End of seeking block
157 }
158
159 //Pup one group of user data
160 void TCharm::UserData::pup(PUP::er &p)
161 {
162   pup_er pext=(pup_er)(&p);
163   p(isC);
164   //Save address of userdata-- assumes user data is on the stack
165   p((void*)&data,sizeof(data));
166   if (isC) { //C version
167     //FIXME: function pointers may not be valid across processors
168     p((void*)&cfn, sizeof(TCpupUserDataC));
169     cfn(pext,data);
170   } 
171   else { //Fortran version
172     //FIXME: function pointers may not be valid across processors
173     p((void*)&ffn, sizeof(TCpupUserDataF));        
174     ffn(pext,data);
175   }
176 }
177
178 TCharm::~TCharm() 
179 {
180   CmiIsomallocBlockListFree(heapBlocks);
181   CthFree(tid);
182   delete initMsg;
183 }
184
185 //Register user data to be packed with the thread
186 int TCharm::add(const TCharm::UserData &d)
187 {
188   if (nUd>=maxUserData)
189     CkAbort("TCharm: Registered too many user data fields!\n");
190   int nu=nUd++;
191   ud[nu]=d;
192   return nu;
193 }
194 void *TCharm::lookupUserData(int i) {
195         if (i<0 || i>=nUd)
196                 CkAbort("Bad user data index passed to TCharmGetUserdata!\n");
197         return ud[i].getData();
198 }
199
200 //Start the thread running
201 void TCharm::run(void)
202 {
203   DBG("TCharm::run()");
204   start();
205 }
206
207 //Block the thread until start()ed again.
208 void TCharm::stop(void)
209 {
210   if (isStopped) return; //Nothing to do
211 #ifndef CMK_OPTIMIZE
212   DBG("suspending thread");
213   if (tid != CthSelf())
214     CkAbort("Called TCharm::stop from outside TCharm thread!\n");
215   if (tcharm_nothreads)
216     CkAbort("Cannot make blocking calls using +tcharm_nothreads!\n");
217 #endif
218   isStopped=true;
219   stopTiming();
220   TCharm::setState(inFramework);
221   CthSuspend();
222   TCharm::setState(inDriver);
223   /*We have to do the get() because "this" may have changed
224     during a migration-suspend.*/
225   TCharm::get()->startTiming();
226 }
227
228 //Resume the waiting thread
229 void TCharm::start(void)
230 {
231   if (!isStopped) return; //Already started
232   isStopped=false;
233   TCharm::setState(inDriver);
234   DBG("awakening thread");
235   if (tcharm_nothreads) /*Call user routine directly*/
236           startTCharmThread(initMsg);
237   else /*Jump to thread normally*/
238           CthAwaken(tid);
239 }
240
241 //Go to sync, block, possibly migrate, and then resume
242 void TCharm::migrate(void)
243 {
244 #if CMK_LBDB_ON
245   DBG("going to sync");  
246   AtSync();
247   stop();
248 #else
249   DBG("skipping sync, because there is no load balancer");
250 #endif
251 }
252
253 //Resume from sync: start the thread again
254 void TCharm::ResumeFromSync(void)
255 {
256   start();
257 }
258
259 #ifndef CMK_OPTIMIZE
260 //Make sure we're actually in driver
261 void TCharm::check(void)
262 {
263         if (getState()!=inDriver)
264                 ::CkAbort("TCharm> Can only use that routine from within driver!\n");
265 }
266 #endif
267
268 static int propMapCreated=0;
269 static CkGroupID propMapID;
270 CkGroupID CkCreatePropMap(void);
271
272 static void TCharmBuildThreads(TCharmInitMsg *msg,TCharmSetupCookie &cook)
273 {
274         CkArrayOptions opts(msg->numElements);
275         if (!propMapCreated) {
276                 propMapCreated=1;
277                 propMapID=CkCreatePropMap();
278         }
279         opts.setMap(propMapID);
280         int nElem=msg->numElements; //<- save it because msg will be deleted.
281         CkArrayID id=CProxy_TCharm::ckNew(msg,opts);
282         cook.setThreads(id,nElem);
283 }
284
285 /****** Readonlys *****/
286 CkVec<TCpupReadonlyGlobal> TCharmReadonlys::entries;
287 void TCharmReadonlys::add(TCpupReadonlyGlobal fn)
288 {
289         entries.push_back(fn);
290 }
291 //Pups all registered readonlys
292 void TCharmReadonlys::pup(PUP::er &p) {
293         if (p.isUnpacking()) {
294                 //HACK: Rather than sending this message only where its needed,
295                 // we send it everywhere and just ignore it if it's not needed.
296                 if (CkMyPe()==0) return; //Processor 0 is the source-- no unpacking needed
297                 if (CkMyRank()!=0) return; //Some other processor will do the unpacking
298         }
299         //Pup the globals for this node:
300         int i,n=entries.length();
301         p(n);
302         if (n!=entries.length())
303                 CkAbort("TCharmReadonly list length mismatch!\n");
304         for (i=0;i<n;i++)
305                 (entries[i])((pup_er)&p);
306 }
307
308 CDECL void TCharmReadonlyGlobals(TCpupReadonlyGlobal fn)
309 {
310         TCHARMAPI("TCharmReadonlyGlobals");
311         if (TCharm::getState()!=inNodeSetup)
312                 CkAbort("Can only call TCharmReadonlyGlobals from in TCharmUserNodeSetup!\n");
313         TCharmReadonlys::add(fn);
314 }
315 FDECL void FTN_NAME(TCHARM_READONLY_GLOBALS,tcharm_readonly_globals)
316         (TCpupReadonlyGlobal fn)
317 {
318         TCharmReadonlyGlobals(fn);
319 }
320
321 /************* Startup/Shutdown Coordination Support ************/
322
323 enum {TC_READY=23, TC_BARRIER=87, TC_DONE=42};
324
325 //Called when a client is ready to run
326 void TCharm::ready(void) {
327         DBG("TCharm thread "<<thisIndex<<" ready")
328         int vals[2]={0,1};
329         if (thisIndex==0) vals[0]=TC_READY;
330         //Contribute to a synchronizing reduction
331         contribute(sizeof(vals),&vals,CkReduction::sum_int);
332 }
333
334 //Called when we want to go to a barrier
335 void TCharm::barrier(void) {
336         int vals[2]={0,1};
337         if (thisIndex==0) vals[0]=TC_BARRIER;
338         //Contribute to a synchronizing reduction
339         contribute(sizeof(vals),&vals,CkReduction::sum_int);
340         stop();
341 }
342
343 //Called when the thread is done running
344 void TCharm::done(void) {
345         DBG("TCharm thread "<<thisIndex<<" done")
346         int vals[2]={0,1};
347         if (thisIndex==0) vals[0]=TC_DONE;
348         //Contribute to a synchronizing reduction
349         contribute(sizeof(vals),&vals,CkReduction::sum_int);
350         stop();
351 }
352
353 //Called when an array reduction is complete
354 static void coordinatorReduction(void *coord_,int dataLen,void *reductionData)
355 {
356         TCharmCoordinator *coord=(TCharmCoordinator *)coord_;
357         int *vals=(int *)reductionData;
358         if (dataLen!=2*sizeof(int))
359                 CkAbort("Unexpected length in TCharm array reduction!\n");
360         DBGX("Finished coordinator reduction: "<<vals[0]<<", "<<vals[1]);
361         switch (vals[0]) {
362         case TC_READY: coord->clientReady(); break;
363         case TC_BARRIER: coord->clientBarrier(); break;
364         case TC_DONE: coord->clientDone(); break;
365         default:
366                 CkAbort("Unexpected value from TCharm array reduction!\n");
367         };
368 }
369
370 int TCharmCoordinator::nArrays=0; //Total number of running thread arrays
371 TCharmCoordinator *TCharmCoordinator::head=NULL; //List of coordinators
372
373
374 TCharmCoordinator::TCharmCoordinator(CkArrayID threads_,int nThreads_)
375         :threads(threads_), nThreads(nThreads_), nClients(0), nReady(0)
376 {
377         nArrays++;
378         //Link into the coordinator list
379         next=head;
380         head=this;
381
382         threads.setReductionClient(coordinatorReduction,this);
383         nClients=1; //Thread array itself is a client
384 }
385 TCharmCoordinator::~TCharmCoordinator()
386 {
387         //Coordinators never get deleted
388 }
389 void TCharmCoordinator::addClient(const CkArrayID &client)
390 {
391         nClients++;
392 }
393 void TCharmCoordinator::clientReady(void)
394 {
395         DBGX("client "<<nReady+1<<" of "<<nClients<<" ready");
396         nReady++;
397         if (nReady>=nClients) { //All client arrays are ready-- start threads
398                 DBGX("starting threads");
399                 threads.run();
400         }
401 }
402 void TCharmCoordinator::clientBarrier(void)
403 {
404         DBGX("clients all at barrier");
405         threads.run();
406 }
407 void TCharmCoordinator::clientDone(void)
408 {
409         DBGX("clientDone");     
410         nArrays--;
411         if (nArrays<=0) { //All arrays have exited
412                 DBGX("done with computation");
413                 CkExit();
414         }
415 }
416
417 /************* Setup **************/
418
419 //Cookie used during setup
420 TCharmSetupCookie *TCharmSetupCookie::theCookie;
421
422 //Globals used to control setup process
423 static int g_numDefaultSetups=0;
424 static TCharmFallbackSetupFn g_fallbackSetup=NULL;
425 void TCharmSetFallbackSetup(TCharmFallbackSetupFn f)
426 {
427         g_fallbackSetup=f;
428 }
429 CDECL void TCharmInDefaultSetup(void) {
430         g_numDefaultSetups++;
431 }
432
433 //Tiny simple main chare
434 class TCharmMain : public Chare {
435 public:
436   TCharmMain(CkArgMsg *msg) {
437     if (0!=(tcharm_nomig=CmiGetArgFlag(msg->argv,"+tcharm_nomig")))
438         CmiPrintf("TCHARM> Disabling migration support, for debugging\n");
439     tcharm_nothreads=CmiGetArgFlag(msg->argv,"+tcharm_nothread");
440     tcharm_nothreads|=CmiGetArgFlag(msg->argv,"+tcharm_nothreads");
441     if (0!=tcharm_nothreads)
442        CmiPrintf("TCHARM> Disabling thread support, for debugging\n");
443     char *traceLibName=NULL;
444     while (CmiGetArgString(msg->argv,"+tcharm_trace",&traceLibName))
445        tcharm_tracelibs.addTracing(traceLibName);
446
447     TCharmSetupCookie cookie(msg->argv);
448     TCharmSetupCookie::theCookie=&cookie;
449     g_numDefaultSetups=0;
450     
451     /*Call user-overridable C setup*/
452     TCharmUserSetup();
453     /*Call user-overridable Fortran setup*/
454     FTN_NAME(TCHARM_USER_SETUP,tcharm_user_setup)();
455     
456     if (g_numDefaultSetups==2) 
457     { //User didn't override either setup routine
458             if (g_fallbackSetup)
459                     (g_fallbackSetup)();
460             else
461                     CmiAbort("You need to override TCharmUserSetup to start your computation, or else link in a framework module\n");
462     }       
463     
464     delete msg;
465     
466     if (0==TCharmCoordinator::getTotal())
467             CkAbort("You didn't create any TCharm arrays in TCharmUserSetup!\n");
468
469     //Send out the readonly globals:
470     TCharmReadonlys r;
471     CProxy_TCharmReadonlyGroup::ckNew(r);
472   }
473 };
474
475 #ifndef CMK_OPTIMIZE
476 /*The setup cookie, used to store global initialization state*/
477 TCharmSetupCookie &TCharmSetupCookie::check(void)
478 {
479         if (magic!=correctMagic)
480                 CkAbort("TCharm setup cookie is damaged!\n");
481         return *this;
482 }
483 #endif
484
485 void TCharmSetupCookie::setThreads(const CkArrayID &aid,int nel)
486 {
487         coord=new TCharmCoordinator(aid,nel);
488         tc=aid; numElements=nel;
489 }
490
491 TCharmSetupCookie::TCharmSetupCookie(char **argv_)
492 {
493         magic=correctMagic;
494         argv=argv_;
495         coord=NULL;
496         stackSize=1*1024*1024; /*Default stack size is 1MB*/
497         CmiGetArgInt(argv,"+tcharm_stacksize",&stackSize);
498 }
499
500
501 /************** User API ***************/
502
503 #define cookie (*TCharmSetupCookie::get())
504
505 /**********************************
506 Callable from UserSetup: 
507 */
508
509 /*Set the size of the thread stack*/
510 CDECL void TCharmSetStackSize(int newStackSize)
511 {
512         TCHARMAPI("TCharmSetStackSize");
513         if (TCharm::getState()!=inInit)
514                 CkAbort("TCharm> Can only set stack size from in init!\n");
515         cookie.setStackSize(newStackSize);
516 }
517 FDECL void FTN_NAME(TCHARM_SET_STACK_SIZE,tcharm_set_stack_size)
518         (int *newSize)
519 { TCharmSetStackSize(*newSize); }
520
521
522 /*Create a new array of threads, which will be bound to by subsequent libraries*/
523 CDECL void TCharmCreate(int nThreads,
524                         TCharmThreadStartFn threadFn)
525 {
526         TCHARMAPI("TCharmCreate");
527         TCharmCreateData(nThreads,
528                          (TCharmThreadDataStartFn)threadFn,NULL,0);
529 }
530 FDECL void FTN_NAME(TCHARM_CREATE,tcharm_create)
531         (int *nThreads,TCharmThreadStartFn threadFn)
532 { TCharmCreate(*nThreads,threadFn); }
533
534
535 /*As above, but pass along (arbitrary) data to threads*/
536 CDECL void TCharmCreateData(int nThreads,
537                   TCharmThreadDataStartFn threadFn,
538                   void *threadData,int threadDataLen)
539 {
540         TCHARMAPI("TCharmCreateData");
541         if (TCharm::getState()!=inInit)
542                 CkAbort("TCharm> Can only create threads from in init!\n");
543         TCharmSetupCookie &cook=cookie;
544         TCharmInitMsg *msg=new (threadDataLen,0) TCharmInitMsg(
545                 (CthVoidFn)threadFn,cook.getStackSize());
546         msg->numElements=nThreads;
547         memcpy(msg->data,threadData,threadDataLen);
548         TCharmBuildThreads(msg,cook);
549 }
550
551 FDECL void FTN_NAME(TCHARM_CREATE_DATA,tcharm_create_data)
552         (int *nThreads,
553                   TCharmThreadDataStartFn threadFn,
554                   void *threadData,int *threadDataLen)
555 { TCharmCreateData(*nThreads,threadFn,threadData,*threadDataLen); }
556
557
558 /*Get the unconsumed command-line arguments*/
559 CDECL char **TCharmArgv(void)
560 {
561         TCHARMAPI("TCharmArgv");
562         if (TCharm::getState()!=inInit)
563                 CkAbort("TCharm> Can only get arguments from in init!\n");
564         return cookie.getArgv();
565 }
566 CDECL int TCharmArgc(void)
567 {
568         TCHARMAPI("TCharmArgc");
569         if (TCharm::getState()!=inInit)
570                 CkAbort("TCharm> Can only get arguments from in init!\n");
571         return CmiGetArgc(cookie.getArgv());
572 }
573
574 CDECL int TCharmGetNumChunks(void)
575 {
576         TCHARMAPI("TCharmGetNumChunks");
577         int nChunks=CkNumPes();
578         char **argv=TCharmArgv();
579         CmiGetArgInt(argv,"-vp",&nChunks);
580         CmiGetArgInt(argv,"+vp",&nChunks);
581         lastNumChunks=nChunks;
582         return nChunks;
583 }
584 FDECL int FTN_NAME(TCHARM_GET_NUM_CHUNKS,tcharm_get_num_chunks)(void)
585 {
586         return TCharmGetNumChunks();
587 }
588
589
590 /***********************************
591 Callable from worker thread
592 */
593 CDECL int TCharmElement(void)
594
595         TCHARMAPI("TCharmElement");
596         return TCharm::get()->getElement();
597 }
598 CDECL int TCharmNumElements(void)
599
600         TCHARMAPI("TCharmNumElements");
601         if (TCharm::getState()==inDriver)
602                 return TCharm::get()->getNumElements();
603         else
604                 return lastNumChunks;
605 }
606
607 FDECL int FTN_NAME(TCHARM_ELEMENT,tcharm_element)(void) 
608 { return TCharmElement();}
609 FDECL int FTN_NAME(TCHARM_NUM_ELEMENTS,tcharm_num_elements)(void) 
610 { return TCharmNumElements();}
611
612 //Make sure this address will migrate with us when we move:
613 static void checkAddress(void *data)
614 {
615         if (tcharm_nomig||tcharm_nothreads) return; //Stack is not isomalloc'd
616         if (!CmiIsomallocInRange(data))
617             CkAbort("The UserData you register must be allocated on the stack!\n");
618 }
619
620 CDECL int TCharmRegister(void *data,TCharmPupFn pfn)
621
622         TCHARMAPI("TCharmRegister");
623         checkAddress(data);
624         return TCharm::get()->add(TCharm::UserData(pfn,data));
625 }
626 FDECL int FTN_NAME(TCHARM_REGISTER,tcharm_register)
627         (void *data,TCpupUserDataF pfn)
628
629         TCHARMAPI("TCharm_Register");
630         checkAddress(data);
631         return TCharm::get()->add(TCharm::UserData(
632                 pfn,data,TCharm::UserData::isFortran()));
633 }
634
635 CDECL void *TCharmGetUserdata(int id)
636 {
637         TCHARMAPI("TCharmGetUserdata");
638         return TCharm::get()->lookupUserData(id);
639 }
640 FDECL void *FTN_NAME(TCHARM_GET_USERDATA,tcharm_get_userdata)(int *id)
641 { return TCharmGetUserdata(*id); }
642
643 CDECL void TCharmMigrate(void)
644 {
645         TCHARMAPI("TCharmMigrate");
646         TCharm::get()->migrate();
647 }
648 FDECL void FTN_NAME(TCHARM_MIGRATE,tcharm_migrate)(void)
649 {
650         TCHARMAPI("TCharmMigrate");
651         TCharm::get()->migrate();
652 }
653
654 CDECL void TCharmBarrier(void)
655 {
656         TCHARMAPI("TCharmBarrier");
657         TCharm::get()->barrier();
658 }
659 FDECL void FTN_NAME(TCHARM_BARRIER,tcharm_barrier)(void)
660 {
661         TCharmBarrier();
662 }
663
664 CDECL void TCharmDone(void)
665 {
666         TCHARMAPI("TCharmDone");
667         TCharm::get()->done();
668 }
669 FDECL void FTN_NAME(TCHARM_DONE,tcharm_done)(void)
670 {
671         TCharmDone();
672 }
673
674
675
676 #include "tcharm.def.h"