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