[RTL/NTDLL]
[reactos.git] / reactos / lib / rtl / critical.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS system libraries
4 * FILE: lib/rtl/critical.c
5 * PURPOSE: Critical sections
6 * PROGRAMMERS: Alex Ionescu (alex@relsoft.net)
7 * Gunnar Dalsnes
8 */
9
10 /* INCLUDES *****************************************************************/
11
12 #include <rtl.h>
13
14 #define NDEBUG
15 #include <debug.h>
16
17 #define MAX_STATIC_CS_DEBUG_OBJECTS 64
18
19 static RTL_CRITICAL_SECTION RtlCriticalSectionLock;
20 static LIST_ENTRY RtlCriticalSectionList;
21 static BOOLEAN RtlpCritSectInitialized = FALSE;
22 static RTL_CRITICAL_SECTION_DEBUG RtlpStaticDebugInfo[MAX_STATIC_CS_DEBUG_OBJECTS];
23 static BOOLEAN RtlpDebugInfoFreeList[MAX_STATIC_CS_DEBUG_OBJECTS];
24 LARGE_INTEGER RtlpTimeout;
25
26 /* FUNCTIONS *****************************************************************/
27
28 /*++
29 * RtlpCreateCriticalSectionSem
30 *
31 * Checks if an Event has been created for the critical section.
32 *
33 * Params:
34 * None
35 *
36 * Returns:
37 * None. Raises an exception if the system call failed.
38 *
39 * Remarks:
40 * None
41 *
42 *--*/
43 _At_(CriticalSection->LockSemaphore, _Post_notnull_)
44 VOID
45 NTAPI
46 RtlpCreateCriticalSectionSem(PRTL_CRITICAL_SECTION CriticalSection)
47 {
48 HANDLE hEvent = CriticalSection->LockSemaphore;
49 HANDLE hNewEvent;
50 NTSTATUS Status;
51
52 /* Check if we have an event */
53 if (!hEvent)
54 {
55
56 /* No, so create it */
57 Status = NtCreateEvent(&hNewEvent,
58 EVENT_ALL_ACCESS,
59 NULL,
60 SynchronizationEvent,
61 FALSE);
62 if (!NT_SUCCESS(Status))
63 {
64 DPRINT1("Failed to Create Event!\n");
65
66 /* Use INVALID_HANDLE_VALUE (-1) to signal that the global
67 keyed event must be used */
68 hNewEvent = INVALID_HANDLE_VALUE;
69 }
70
71 DPRINT("Created Event: %p \n", hNewEvent);
72
73 /* Exchange the LockSemaphore field with the new handle, if it is still 0 */
74 if (InterlockedCompareExchangePointer((PVOID*)&CriticalSection->LockSemaphore,
75 (PVOID)hNewEvent,
76 NULL) != NULL)
77 {
78 /* Someone else just created an event */
79 if (hEvent != INVALID_HANDLE_VALUE)
80 {
81 DPRINT("Closing already created event: %p\n", hNewEvent);
82 NtClose(hNewEvent);
83 }
84 }
85 }
86
87 return;
88 }
89
90 /*++
91 * RtlpWaitForCriticalSection
92 *
93 * Slow path of RtlEnterCriticalSection. Waits on an Event Object.
94 *
95 * Params:
96 * CriticalSection - Critical section to acquire.
97 *
98 * Returns:
99 * STATUS_SUCCESS, or raises an exception if a deadlock is occuring.
100 *
101 * Remarks:
102 * None
103 *
104 *--*/
105 NTSTATUS
106 NTAPI
107 RtlpWaitForCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
108 {
109 NTSTATUS Status;
110 EXCEPTION_RECORD ExceptionRecord;
111 BOOLEAN LastChance = FALSE;
112
113 /* Do we have an Event yet? */
114 if (!CriticalSection->LockSemaphore) {
115 RtlpCreateCriticalSectionSem(CriticalSection);
116 }
117
118 /* Increase the Debug Entry count */
119 DPRINT("Waiting on Critical Section Event: %p %p\n",
120 CriticalSection,
121 CriticalSection->LockSemaphore);
122
123 if (CriticalSection->DebugInfo)
124 CriticalSection->DebugInfo->EntryCount++;
125
126 for (;;) {
127
128 /* Increase the number of times we've had contention */
129 if (CriticalSection->DebugInfo)
130 CriticalSection->DebugInfo->ContentionCount++;
131
132 /* Check if allocating the event failed */
133 if (CriticalSection->LockSemaphore == INVALID_HANDLE_VALUE)
134 {
135 /* Use the global keyed event (NULL as keyed event handle) */
136 Status = NtWaitForKeyedEvent(NULL,
137 CriticalSection,
138 FALSE,
139 &RtlpTimeout);
140 }
141 else
142 {
143 /* Wait on the Event */
144 Status = NtWaitForSingleObject(CriticalSection->LockSemaphore,
145 FALSE,
146 &RtlpTimeout);
147 }
148
149 /* We have Timed out */
150 if (Status == STATUS_TIMEOUT) {
151
152 /* Is this the 2nd time we've timed out? */
153 if (LastChance) {
154
155 DPRINT1("Deadlock: %p\n", CriticalSection);
156
157 /* Yes it is, we are raising an exception */
158 ExceptionRecord.ExceptionCode = STATUS_POSSIBLE_DEADLOCK;
159 ExceptionRecord.ExceptionFlags = 0;
160 ExceptionRecord.ExceptionRecord = NULL;
161 ExceptionRecord.ExceptionAddress = RtlRaiseException;
162 ExceptionRecord.NumberParameters = 1;
163 ExceptionRecord.ExceptionInformation[0] = (ULONG_PTR)CriticalSection;
164 RtlRaiseException(&ExceptionRecord);
165
166 }
167
168 /* One more try */
169 LastChance = TRUE;
170
171 } else {
172
173 /* If we are here, everything went fine */
174 return STATUS_SUCCESS;
175 }
176 }
177 }
178
179 /*++
180 * RtlpUnWaitCriticalSection
181 *
182 * Slow path of RtlLeaveCriticalSection. Fires an Event Object.
183 *
184 * Params:
185 * CriticalSection - Critical section to release.
186 *
187 * Returns:
188 * None. Raises an exception if the system call failed.
189 *
190 * Remarks:
191 * None
192 *
193 *--*/
194 VOID
195 NTAPI
196 RtlpUnWaitCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
197 {
198 NTSTATUS Status;
199
200 /* Do we have an Event yet? */
201 if (!CriticalSection->LockSemaphore) {
202 RtlpCreateCriticalSectionSem(CriticalSection);
203 }
204
205 /* Signal the Event */
206 DPRINT("Signaling Critical Section Event: %p, %p\n",
207 CriticalSection,
208 CriticalSection->LockSemaphore);
209
210 /* Check if this critical section needs to use the keyed event */
211 if (CriticalSection->LockSemaphore == INVALID_HANDLE_VALUE)
212 {
213 /* Release keyed event */
214 Status = NtReleaseKeyedEvent(NULL, CriticalSection, FALSE, &RtlpTimeout);
215 }
216 else
217 {
218 /* Set the event */
219 Status = NtSetEvent(CriticalSection->LockSemaphore, NULL);
220 }
221
222 if (!NT_SUCCESS(Status)) {
223
224 /* We've failed */
225 DPRINT1("Signaling Failed for: %p, %p, 0x%08lx\n",
226 CriticalSection,
227 CriticalSection->LockSemaphore,
228 Status);
229 RtlRaiseStatus(Status);
230 }
231 }
232
233 /*++
234 * RtlpInitDeferedCriticalSection
235 *
236 * Initializes the Critical Section implementation.
237 *
238 * Params:
239 * None
240 *
241 * Returns:
242 * None.
243 *
244 * Remarks:
245 * After this call, the Process Critical Section list is protected.
246 *
247 *--*/
248 VOID
249 NTAPI
250 RtlpInitDeferedCriticalSection(VOID)
251 {
252
253 /* Initialize the Process Critical Section List */
254 InitializeListHead(&RtlCriticalSectionList);
255
256 /* Initialize the CS Protecting the List */
257 RtlInitializeCriticalSection(&RtlCriticalSectionLock);
258
259 /* It's now safe to enter it */
260 RtlpCritSectInitialized = TRUE;
261 }
262
263 /*++
264 * RtlpAllocateDebugInfo
265 *
266 * Finds or allocates memory for a Critical Section Debug Object
267 *
268 * Params:
269 * None
270 *
271 * Returns:
272 * A pointer to an empty Critical Section Debug Object.
273 *
274 * Remarks:
275 * For optimization purposes, the first 64 entries can be cached. From
276 * then on, future Critical Sections will allocate memory from the heap.
277 *
278 *--*/
279 PRTL_CRITICAL_SECTION_DEBUG
280 NTAPI
281 RtlpAllocateDebugInfo(VOID)
282 {
283 ULONG i;
284
285 /* Try to allocate from our buffer first */
286 for (i = 0; i < MAX_STATIC_CS_DEBUG_OBJECTS; i++) {
287
288 /* Check if Entry is free */
289 if (!RtlpDebugInfoFreeList[i]) {
290
291 /* Mark entry in use */
292 DPRINT("Using entry: %lu. Buffer: %p\n", i, &RtlpStaticDebugInfo[i]);
293 RtlpDebugInfoFreeList[i] = TRUE;
294
295 /* Use free entry found */
296 return &RtlpStaticDebugInfo[i];
297 }
298
299 }
300
301 /* We are out of static buffer, allocate dynamic */
302 return RtlAllocateHeap(RtlGetProcessHeap(),
303 0,
304 sizeof(RTL_CRITICAL_SECTION_DEBUG));
305 }
306
307 /*++
308 * RtlpFreeDebugInfo
309 *
310 * Frees the memory for a Critical Section Debug Object
311 *
312 * Params:
313 * DebugInfo - Pointer to Critical Section Debug Object to free.
314 *
315 * Returns:
316 * None.
317 *
318 * Remarks:
319 * If the pointer is part of the static buffer, then the entry is made
320 * free again. If not, the object is de-allocated from the heap.
321 *
322 *--*/
323 VOID
324 NTAPI
325 RtlpFreeDebugInfo(PRTL_CRITICAL_SECTION_DEBUG DebugInfo)
326 {
327 SIZE_T EntryId;
328
329 /* Is it part of our cached entries? */
330 if ((DebugInfo >= RtlpStaticDebugInfo) &&
331 (DebugInfo <= &RtlpStaticDebugInfo[MAX_STATIC_CS_DEBUG_OBJECTS-1])) {
332
333 /* Yes. zero it out */
334 RtlZeroMemory(DebugInfo, sizeof(RTL_CRITICAL_SECTION_DEBUG));
335
336 /* Mark as free */
337 EntryId = (DebugInfo - RtlpStaticDebugInfo);
338 DPRINT("Freeing from Buffer: %p. Entry: %Iu inside Process: %p\n",
339 DebugInfo,
340 EntryId,
341 NtCurrentTeb()->ClientId.UniqueProcess);
342 RtlpDebugInfoFreeList[EntryId] = FALSE;
343
344 } else if (!DebugInfo->Flags) {
345
346 /* It's a dynamic one, so free from the heap */
347 DPRINT("Freeing from Heap: %p inside Process: %p\n",
348 DebugInfo,
349 NtCurrentTeb()->ClientId.UniqueProcess);
350 RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, DebugInfo);
351
352 } else {
353
354 /* Wine stores a section name pointer in the Flags member */
355 DPRINT("Assuming static: %p inside Process: %p\n",
356 DebugInfo,
357 NtCurrentTeb()->ClientId.UniqueProcess);
358
359 }
360 }
361
362 /*++
363 * RtlDeleteCriticalSection
364 * @implemented NT4
365 *
366 * Deletes a Critical Section
367 *
368 * Params:
369 * CriticalSection - Critical section to delete.
370 *
371 * Returns:
372 * STATUS_SUCCESS, or error value returned by NtClose.
373 *
374 * Remarks:
375 * The critical section members should not be read after this call.
376 *
377 *--*/
378 NTSTATUS
379 NTAPI
380 RtlDeleteCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
381 {
382 NTSTATUS Status = STATUS_SUCCESS;
383
384 DPRINT("Deleting Critical Section: %p\n", CriticalSection);
385 /* Close the Event Object Handle if it exists */
386 if (CriticalSection->LockSemaphore) {
387
388 /* In case NtClose fails, return the status */
389 Status = NtClose(CriticalSection->LockSemaphore);
390
391 }
392
393 /* Protect List */
394 RtlEnterCriticalSection(&RtlCriticalSectionLock);
395
396 if (CriticalSection->DebugInfo)
397 {
398 /* Remove it from the list */
399 RemoveEntryList(&CriticalSection->DebugInfo->ProcessLocksList);
400 #if 0 /* We need to preserve Flags for RtlpFreeDebugInfo */
401 RtlZeroMemory(CriticalSection->DebugInfo, sizeof(RTL_CRITICAL_SECTION_DEBUG));
402 #endif
403 }
404
405 /* Unprotect */
406 RtlLeaveCriticalSection(&RtlCriticalSectionLock);
407
408 if (CriticalSection->DebugInfo)
409 {
410 /* Free it */
411 RtlpFreeDebugInfo(CriticalSection->DebugInfo);
412 }
413
414 /* Wipe it out */
415 RtlZeroMemory(CriticalSection, sizeof(RTL_CRITICAL_SECTION));
416
417 /* Return */
418 return Status;
419 }
420
421 /*++
422 * RtlSetCriticalSectionSpinCount
423 * @implemented NT4
424 *
425 * Sets the spin count for a critical section.
426 *
427 * Params:
428 * CriticalSection - Critical section to set the spin count for.
429 *
430 * SpinCount - Spin count for the critical section.
431 *
432 * Returns:
433 * STATUS_SUCCESS.
434 *
435 * Remarks:
436 * SpinCount is ignored on single-processor systems.
437 *
438 *--*/
439 ULONG
440 NTAPI
441 RtlSetCriticalSectionSpinCount(PRTL_CRITICAL_SECTION CriticalSection,
442 ULONG SpinCount)
443 {
444 ULONG OldCount = (ULONG)CriticalSection->SpinCount;
445
446 /* Set to parameter if MP, or to 0 if this is Uniprocessor */
447 CriticalSection->SpinCount = (NtCurrentPeb()->NumberOfProcessors > 1) ? SpinCount : 0;
448 return OldCount;
449 }
450
451 /*++
452 * RtlEnterCriticalSection
453 * @implemented NT4
454 *
455 * Waits to gain ownership of the critical section.
456 *
457 * Params:
458 * CriticalSection - Critical section to wait for.
459 *
460 * Returns:
461 * STATUS_SUCCESS.
462 *
463 * Remarks:
464 * Uses a fast-path unless contention happens.
465 *
466 *--*/
467 NTSTATUS
468 NTAPI
469 RtlEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
470 {
471 HANDLE Thread = (HANDLE)NtCurrentTeb()->ClientId.UniqueThread;
472
473 /* Try to Lock it */
474 if (InterlockedIncrement(&CriticalSection->LockCount) != 0) {
475
476 /*
477 * We've failed to lock it! Does this thread
478 * actually own it?
479 */
480 if (Thread == CriticalSection->OwningThread) {
481
482 /* You own it, so you'll get it when you're done with it! No need to
483 use the interlocked functions as only the thread who already owns
484 the lock can modify this data. */
485 CriticalSection->RecursionCount++;
486 return STATUS_SUCCESS;
487 }
488
489 /* NOTE - CriticalSection->OwningThread can be NULL here because changing
490 this information is not serialized. This happens when thread a
491 acquires the lock (LockCount == 0) and thread b tries to
492 acquire it as well (LockCount == 1) but thread a hasn't had a
493 chance to set the OwningThread! So it's not an error when
494 OwningThread is NULL here! */
495
496 /* We don't own it, so we must wait for it */
497 RtlpWaitForCriticalSection(CriticalSection);
498 }
499
500 /* Lock successful. Changing this information has not to be serialized because
501 only one thread at a time can actually change it (the one who acquired
502 the lock)! */
503 CriticalSection->OwningThread = Thread;
504 CriticalSection->RecursionCount = 1;
505 return STATUS_SUCCESS;
506 }
507
508 /*++
509 * RtlInitializeCriticalSection
510 * @implemented NT4
511 *
512 * Initialises a new critical section.
513 *
514 * Params:
515 * CriticalSection - Critical section to initialise
516 *
517 * Returns:
518 * STATUS_SUCCESS.
519 *
520 * Remarks:
521 * Simply calls RtlInitializeCriticalSectionAndSpinCount
522 *
523 *--*/
524 NTSTATUS
525 NTAPI
526 RtlInitializeCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
527 {
528 /* Call the Main Function */
529 return RtlInitializeCriticalSectionAndSpinCount(CriticalSection, 0);
530 }
531
532 /*++
533 * RtlInitializeCriticalSectionAndSpinCount
534 * @implemented NT4
535 *
536 * Initialises a new critical section.
537 *
538 * Params:
539 * CriticalSection - Critical section to initialise
540 *
541 * SpinCount - Spin count for the critical section.
542 *
543 * Returns:
544 * STATUS_SUCCESS.
545 *
546 * Remarks:
547 * SpinCount is ignored on single-processor systems.
548 *
549 *--*/
550 NTSTATUS
551 NTAPI
552 RtlInitializeCriticalSectionAndSpinCount(PRTL_CRITICAL_SECTION CriticalSection,
553 ULONG SpinCount)
554 {
555 PRTL_CRITICAL_SECTION_DEBUG CritcalSectionDebugData;
556
557 /* First things first, set up the Object */
558 DPRINT("Initializing Critical Section: %p\n", CriticalSection);
559 CriticalSection->LockCount = -1;
560 CriticalSection->RecursionCount = 0;
561 CriticalSection->OwningThread = 0;
562 CriticalSection->SpinCount = (NtCurrentPeb()->NumberOfProcessors > 1) ? SpinCount : 0;
563 CriticalSection->LockSemaphore = 0;
564
565 /* Allocate the Debug Data */
566 CritcalSectionDebugData = RtlpAllocateDebugInfo();
567 DPRINT("Allocated Debug Data: %p inside Process: %p\n",
568 CritcalSectionDebugData,
569 NtCurrentTeb()->ClientId.UniqueProcess);
570
571 if (!CritcalSectionDebugData) {
572
573 /* This is bad! */
574 DPRINT1("Couldn't allocate Debug Data for: %p\n", CriticalSection);
575 return STATUS_NO_MEMORY;
576 }
577
578 /* Set it up */
579 CritcalSectionDebugData->Type = RTL_CRITSECT_TYPE;
580 CritcalSectionDebugData->ContentionCount = 0;
581 CritcalSectionDebugData->EntryCount = 0;
582 CritcalSectionDebugData->CriticalSection = CriticalSection;
583 CritcalSectionDebugData->Flags = 0;
584 CriticalSection->DebugInfo = CritcalSectionDebugData;
585
586 /*
587 * Add it to the List of Critical Sections owned by the process.
588 * If we've initialized the Lock, then use it. If not, then probably
589 * this is the lock initialization itself, so insert it directly.
590 */
591 if ((CriticalSection != &RtlCriticalSectionLock) && (RtlpCritSectInitialized)) {
592
593 DPRINT("Securely Inserting into ProcessLocks: %p, %p, %p\n",
594 &CritcalSectionDebugData->ProcessLocksList,
595 CriticalSection,
596 &RtlCriticalSectionList);
597
598 /* Protect List */
599 RtlEnterCriticalSection(&RtlCriticalSectionLock);
600
601 /* Add this one */
602 InsertTailList(&RtlCriticalSectionList, &CritcalSectionDebugData->ProcessLocksList);
603
604 /* Unprotect */
605 RtlLeaveCriticalSection(&RtlCriticalSectionLock);
606
607 } else {
608
609 DPRINT("Inserting into ProcessLocks: %p, %p, %p\n",
610 &CritcalSectionDebugData->ProcessLocksList,
611 CriticalSection,
612 &RtlCriticalSectionList);
613
614 /* Add it directly */
615 InsertTailList(&RtlCriticalSectionList, &CritcalSectionDebugData->ProcessLocksList);
616 }
617
618 return STATUS_SUCCESS;
619 }
620
621 /*++
622 * RtlGetCriticalSectionRecursionCount
623 * @implemented NT5.2 SP1
624 *
625 * Retrieves the recursion count of a given critical section.
626 *
627 * Params:
628 * CriticalSection - Critical section to retrieve its recursion count.
629 *
630 * Returns:
631 * The recursion count.
632 *
633 * Remarks:
634 * We return the recursion count of the critical section if it is owned
635 * by the current thread, and otherwise we return zero.
636 *
637 *--*/
638 LONG
639 NTAPI
640 RtlGetCriticalSectionRecursionCount(PRTL_CRITICAL_SECTION CriticalSection)
641 {
642 if (CriticalSection->OwningThread == NtCurrentTeb()->ClientId.UniqueThread)
643 {
644 /*
645 * The critical section is owned by the current thread,
646 * therefore retrieve its actual recursion count.
647 */
648 return CriticalSection->RecursionCount;
649 }
650 else
651 {
652 /*
653 * It is not owned by the current thread, so
654 * for this thread there is no recursion.
655 */
656 return 0;
657 }
658 }
659
660 /*++
661 * RtlLeaveCriticalSection
662 * @implemented NT4
663 *
664 * Releases a critical section and makes if available for new owners.
665 *
666 * Params:
667 * CriticalSection - Critical section to release.
668 *
669 * Returns:
670 * STATUS_SUCCESS.
671 *
672 * Remarks:
673 * If another thread was waiting, the slow path is entered.
674 *
675 *--*/
676 NTSTATUS
677 NTAPI
678 RtlLeaveCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
679 {
680 #if DBG
681 HANDLE Thread = (HANDLE)NtCurrentTeb()->ClientId.UniqueThread;
682
683 /* In win this case isn't checked. However it's a valid check so it should only
684 be performed in debug builds! */
685 if (Thread != CriticalSection->OwningThread)
686 {
687 DPRINT1("Releasing critical section not owned!\n");
688 return STATUS_INVALID_PARAMETER;
689 }
690 #endif
691
692 /* Decrease the Recursion Count. No need to do this atomically because only
693 the thread who holds the lock can call this function (unless the program
694 is totally screwed... */
695 if (--CriticalSection->RecursionCount) {
696
697 /* Someone still owns us, but we are free. This needs to be done atomically. */
698 InterlockedDecrement(&CriticalSection->LockCount);
699
700 } else {
701
702 /* Nobody owns us anymore. No need to do this atomically. See comment
703 above. */
704 CriticalSection->OwningThread = 0;
705
706 /* Was someone wanting us? This needs to be done atomically. */
707 if (-1 != InterlockedDecrement(&CriticalSection->LockCount)) {
708
709 /* Let him have us */
710 RtlpUnWaitCriticalSection(CriticalSection);
711 }
712 }
713
714 /* Sucessful! */
715 return STATUS_SUCCESS;
716 }
717
718 /*++
719 * RtlTryEnterCriticalSection
720 * @implemented NT4
721 *
722 * Attemps to gain ownership of the critical section without waiting.
723 *
724 * Params:
725 * CriticalSection - Critical section to attempt acquiring.
726 *
727 * Returns:
728 * TRUE if the critical section has been acquired, FALSE otherwise.
729 *
730 * Remarks:
731 * None
732 *
733 *--*/
734 BOOLEAN
735 NTAPI
736 RtlTryEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
737 {
738 /* Try to take control */
739 if (InterlockedCompareExchange(&CriticalSection->LockCount,
740 0,
741 -1) == -1) {
742
743 /* It's ours */
744 CriticalSection->OwningThread = NtCurrentTeb()->ClientId.UniqueThread;
745 CriticalSection->RecursionCount = 1;
746 return TRUE;
747
748 } else if (CriticalSection->OwningThread == NtCurrentTeb()->ClientId.UniqueThread) {
749
750 /* It's already ours */
751 InterlockedIncrement(&CriticalSection->LockCount);
752 CriticalSection->RecursionCount++;
753 return TRUE;
754 }
755
756 /* It's not ours */
757 return FALSE;
758 }
759
760 VOID
761 NTAPI
762 RtlCheckForOrphanedCriticalSections(
763 HANDLE ThreadHandle)
764 {
765 UNIMPLEMENTED;
766 }
767
768 /* EOF */