onlinedokumentation

How to: Realtime with C#

Writing kernel functions in C# is not possible, but getting functions written in C++ executed on the kernel level is very easy. Mechanisms like shared memory or Events allow you to synchronize the kernel level and application level or let you exchange data between both parts.

The sample VS2005or8_CSharp_with_WPF shows a WPF application written in C# that loads a DLL to the kernel level and starts a real-time timer with a callback function executed on kernel level, although the user interface is implemented in C#.

The timer callback increments a counter which is located in Shared Memory and read by the application. The read operation is invoked in two different ways.

  1. A timer is started on application level and in its event procedure reads the counter value which is written to a textbox.
  2. The counter value is also read from Shared Memory but this code sequence is invoked by an event set on kernel level.

The code of the DLL

Before we have a look at the code of the C# application, lets see what happens in the DLL:

// C/C++

// User defined data structure
struct TimerData
{ int counter_;
  Handle hEvent_;
  bool syncEvent_;
};

bool _init;

// An optional initialization function
extern "C" Error __declspec(dllexport) __stdcall _initFunction(void* pArgs)
{
  _init = true;
  return KS_OK;
}
// Callback function
extern "C" Error __declspec(dllexport) __stdcall _timerCallBack(void* pArgs, void* pContext)
{
  if( !pArgs )
    return KSERROR_BAD_PARAM;

  TimerData* pData = (TimerData*)pArgs;

  ++pData->counter_;
  
  if(pData->syncEvent_ && pData->counter_ % 500 == 0)
    KS_setEvent(pData->hEvent_);

  return KS_OK;
}

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

// DLLMain function is only a placeholder
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID pReserved)
{ return 1;
}

  • TimerData is used to save data in Shared Memory
  • An initialization function is optional an can be called while loading the DLL to kernel level
  • The callback function is called when timer period is over. It only increments a counter and if necessary sets an event
  • DllMain is only a placeholder because it is not called

Initialization

The initialization can be found in the function init in the code-file of the window VS2005or8_CSharp_with_WPF.xaml.cs. We have a look at the following steps of the init function

  1. If possible switch to Time Stamp Counter (TSC) as hardware timer
  2. Create event for sending signals to application
  3. Create Shared Memory to exchange data between kernel level and application
  4. Load Kernel.dll to ring 0
  5. Create timer callback
  6. Set timer resolution and create the timer

After the Kithara driver is opened (don't forget to pass your customer number) we switch to the TSC as hardware counter which provides realtime features.

ksError = Kithara.KS_calibrateMachineTime(null, 0);
if (ksError != 0) {
 outputErr(ksError, "KS_calibrateMachineTime", "Unable to calibrate the PC's TSC!");
}
Calibration is already done while opening driver but for compatibility reasons KS_calibrateMaschineTime is used for switching. First parameter can be a pointer to an unsigned integer value representing the frequency, therefore flags have to be set to KSF_USE_GIVEN_FREQ.

Event and Shared Memory

Creating the event is very easy by calling KS_createEvent:

Kithara.Handle hEvent;
ksError = Kithara.KS_createEvent(
                  &hEvent,                                // Address of event handle
                  "MyVS2005or08_CSharp_with_WPFEvent",    // Name of the event
                  0);                                     // Flags, here 0
hEvent_ = hEvent;
If you have a valid event-handle you can call KS_waitForEvent for blocking a thread and wait for the event to be set. So other threads could call KS_setEvent/KS_pulseEvent to set the event which causes the blocked thread to resume.
Why you are not allowed to write directly to the class member hEvent_ is described in C# support.
We create Shared Memory to exchange data between kernel level and application level. Call KS_createSharedMem:
TimerData* pAppPtr;
TimerData* pSysPtr;
ksError = Kithara.KS_createSharedMem(
                  (void**)&pAppPtr,                      // Address of application pointer
                  (void**)&pSysPtr,                      // Address of kernel pointer
                  "MyVS2005or08_CSharp_with_WPFMem",     // Name of shared memory area
                  (uint)sizeof(TimerData),               // Size of shared memory area
                  0);                                    // Flags, here 0 (see manual)

pAppPtr_->hEvent_ = hEvent_;                             // Save the created event
pAppPtr_->counter_ = 0;                                  // Clear the timer counter

Pay attention to the two different pointers pAppPtr and pSysPtr. First one is for access from application (C#-Part) and pSysPtr is for access from kernel level. Thats why the hEvent and counter_ is set via pAppPtr_.

Loading the DLL and create callback

Now we come to the part where we load the Kernel.dll to system address space. Make sure that the DLL is located in one of the search paths othwerwise the init process must be aborted. KS_loadKernel can also call a initialization function in the Dll. Remember, we specified one named _initFunction, so as the parameter for the init function we specify the TimerData structure located in SharedMemory. Flags, here KSF_DIRECT_EXEC tells the Kithara driver that the DLL shall be loaded to kernel level.

Handle hKernel;
ksError = Kithara.KS_loadKernel(
                  &hKernel,                               // Kernel handle
                  "Kernel.dll",                           // Name of DLL
                  "_initFunction",                        // Name of init routine
                  pSysPtr_,                               // Init parameter
                  KSF_DIRECT_EXEC);                       // Flags

So now we only have to register our callback function, use KS_createKernelCallBack:

Kithara.Handle hCallBack;
ksError = Kithara.KS_createKernelCallBack(
                  &hCallBack,                             // Address of callback handle
                  hKernel_,                               // Handle of kernel DLL
                  "_timerCallBack",                       // Name of callback function
                  pSysPtr_,                               // Reference parameter to the callback
                  KSF_DIRECT_EXEC,                        // Flags, here kernel level
                  16);                                    // Priority, only used on Ring3
On success hCallBack is the valid handle to the callback and will be used to register the timer callback. hKernel_ is the handle to the DLL which was loaded in the previous step. Pay attention to the parameter #4, the parameter which is passed to the callback function. Here we use pSysPtr_, the system pointer to the Shared Memory. The function is called on kernel level, so it needs the correct pointer to the Shared Memory. Again flags are set to KSF_DIRECT_EXEC.

Create timer

Last but not least we set the resolution for the hardware timer, which is the base for all registered timer. It is necessary to set this frequency at least equal or higher than the timer frequency. KS_createTimer registers the realtime timer and writes its handle back to hTimer. Timer period is set to one millisecond and the callback handle hCallBack is passed as signalization object. Important is the flag KSF_REALTIMER_EXEC which means the timer can handle timer periods less than one millisecond. KSF_DONT_START is combined to say, that the timer is started manually with KS_startTimer.

ksError = Kithara.KS_setTimerResolution(100 * us, 0);
if (ksError != 0) {
  outputErr(ksError, "KS_setTimerResolution", "Unable to set the timer resolution!");
  Kithara.KS_closeDriver();
  return false;
}

Kithara.Handle hTimer;
ksError = Kithara.KS_createTimer(
                  &hTimer,                                // Address of timer handle
                  1 * ms,                                 // Timer period in 100-ns-units
                  hCallBack_,                             // Callback handle
                  Kithara.KSF_REALTIME_EXEC 
                      | Kithara.KSF_DONT_START);          // Flags

Read counter from Application

As mentioned at the beginning of the article there are two ways of invoking the read operation of the counter. An application timer which event procedure reads the counter or a seperate thread which is signalled by an event set on kernel level.

Thread signalled by event

The seperate .NET thread waits for an event set by the callback routine and invokes a write operation to the control on the WPF edit control. The thread is started in the button2__Click event procedure:

eventThread_ = new Thread(new ThreadStart(threadStart));
eventThread_.Name = "TimerWatcherThread.";
hWait_ = new ManualResetEvent(false);
eventThread_.Start();
To pause and resume the thread we use a ManualResetEvent, so do not mix it up with the Kithara event which give the signal to read the counter.
The thread function is threadStart and runs while the terminate_ variable is false. But it mainly waits for the Kithara event:
ksError = Kithara.KS_waitForEvent(        // Wait for event signaled by kernel function
                      hEvent_,            // EventHandle returned by KS_createEvent
                      0,                  // Flags currently not used. Pass 0
                      1000 * ms);         // Timeout. Pass 0 to wait infinite time
KS_waitForEvent blocks the thread and returns after event was set or a timeout occured. We set the timeout to 1s so the thread can check for terminate/pause request. If the timeout occurs ksError equals KSERROR_WAIT_TIMEOUT and we log the timeout but only once so we do not spam our log edit control.
if (ksError != Kithara.KS_OK && ksLastError != ksError) {
  if ((ksError & errMask) == Kithara.KSERROR_WAIT_TIMEOUT) {
    logMsg = "Timeout while waiting for event";
  }
 // [...]
} 
else if (ksError == Kithara.KS_OK) {
  outputEventCounter((pAppPtr_->counter_).ToString());       // Output counter
}
Lets have a look at the function ouputEventCounter. Because we cannot access UI controls from other threads they were created we have to synchronize this access. If we have to synchronize we add the operation to the queue of the Dispatcher belonging to the UI thread.
if (Dispatcher.Thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId) {    
  Dispatcher.BeginInvoke(                                    // Asynchronous invoke
                 new SyncDelegate(outputEventCounter),       // Delegate to function
                 new object[] { value });                    // Parameter, array of object
  return;
}
The function will be invoked asynchronously so the TimerWatcherThread won't be blocked. Synchronize operation and write operation are implemented in the same function.

Cleanup

The cleanup code can be found in the function clean and it is invoked before the WPF window closes. The cleanup process includes the following Kithara driver related actions:

Action Kithara function Parameter
Remove timer KS_removeTimer Timer handle
Remove callback KS_removeCallback Callback handle
Close event KS_closeEvent Event handle
Free Shared Memory KS_freeSharedMem Application pointer
Free the Kernel.dllKS_freeKernel DLL handle
Close the Kithara driverKS_closeDriver

The TimerWatcherThread is terminated by setting the corresponding flag, if it is not terminated after one second an abort request is sent which causes a ThreadAbortException to be raised in the thread.

if (eventThread_ != null) {
  terminate_ = true;
  eventThread_.Join(1000);
  if (eventThread_.IsAlive) {
    eventThread_.Abort();                                // Hard thread abort
    eventThread_.Join();                                 // Wait for thread to join  
  }
}