C# 支持
目录
尽管用 C# 编写的应用程序在 .NET 环境中运行,但可以访问 Kithara 驱动程序。若要使用 Kithara 驱动程序正确运行 C# 项目,必须进行一些准备工作。
其他主题
- Realtime with C# - 如何使用 C# 在内核级别运行本机代码?
准备 Visual Studio C# 项目
提供了一个包装类,用于从 C# 访问 Kithara 驱动程序。包装类包含用于 Kithara 软件的常量、结构和函数。该Kxxxx.cs包含包装类,可以在安装根目录的子目录 dev 中找到。xxxx 代表您购买的 Kithara 内核版本。有关随 Kithara 软件一起安装的文件和目录的更多信息,请查看文档第 2 章。
将包装类添加到项目中
要在项目中使用包装类,只需向其添加Kxxxx.cs即可。dev 目录还包含一个名为Kithara.cs
的文件。这是一个代理类,有助于访问包装类。有关详细信息,请参阅Kithara.cs
中的注释。
访问 Kithara 驱动程序的 C# 应用程序包含 unsafe 上下文,因此必须使用设置了 /unsafe 标志的 Kithara 驱动程序编译 C# 项目。
如何在 Visual Studio 中设置 /unsafe 标志
-
在 Visual Studio 中打开项目
-
在项目资源管理器中右键单击您的项目文件,将打开项目属性
-
选择左侧的构建选项卡
-
勾选
允许不安全代码
垃圾回收 (GC)
请记住.NET 的 GC 可能会在内存中移动变量,而 Kithara 驱动程序使用指向它们的指针。因此,有必要确保在 Kithara 驱动程序(非托管代码)使用这些指针时变量不会移动。首先,哪些变量是可移动的?
通常,存储在堆内存上的所有变量都是可移动的。
最重要的案例是:
-
引用类型的实例
-
类的每个(静态)成员,即使它是一个值类型
-
ref/out 传递的参数
以下情况可以视为固定变量:
-
局部变量或值参数,只要不被匿名方法捕获
-
可以认为是固定的结构构件
-
您可以通过指针访问的变量及其成员
有关固定变量和可移动变量的更多信息,请参见 ECMA C# 语言规范 第 27.3 章。
修复了单函数调用的变量
只要 Kithara 驱动程序使用指向该变量的指针,就必须固定该变量。如果调用的函数仅在调用指针时才使用指针,则只需为这一次调用传递一个固定变量。例如,所有需要指向句柄类型的指针的 Kithara 函数仅在函数执行时使用此指针。此外,KS_createSharedMem
需要指向变量的指针,它可以将所定位内存的地址写入该变量。返回到代码序列后,Kithara 驱动程序没有保存指针的副本,因此无需再修复变量。
请看以下示例:
public unsafe class Class1 : Kithara
{
public Handle hEvent_;
public void f() {
// [...]
Handle hEvent;
KS_createEvent(&hEvent, `MyEvent`, 0);
hEvent_ = hEvent;
// [...]
}
}
因为 hEvent_ 是 Class1 的成员,所以它是一个可移动的变量,无法获取其地址。这就是为什么我们临时使用局部变量来存储句柄的原因。之后,将内容写给班级成员。甚至不可能从成员 hEvent 获取地址 — 编译器会引发错误 CS0212。
另一种方法是在 fixed语句中使用 hEvent。
一段时间内固定变量
将数据存储在 GC 无法移动的内存块中的一种简单而舒适的方法是创建共享内存**。虽然它可能不用于应用程序和内核级之间的数据交换,但它使用起来既安全又舒适。
在以下示例中,回调函数接收指向用户数据的指针。
public unsafe class CallBackClass : Kithara {
public struct TestData {
public int counter_;
public char value_;
}
// Class members
public TestData* pAppData_;
public TestData* pSysData_;
public Handle hCallBack_;
// The callback function
public int callBackFunction(void* pArgs, void* pContext) {
TestData* pData = (TestData*)pArgs;
++pData->counter_;
return KS_OK;
}
// [...]
指向用户数据的指针必须始终保持有效,只要 Kithara 驱动程序可以调用回调函数。因此,我们在类的其他地方创建共享内存。
// [...]
public void createMemory() {
int ksError;
// Fixed variables for pointers
TestData* pApp,pSys;
ksError = KS_createSharedMem(
(void**)&pApp, // Application pointer
(void**)&pSys, // System pointer
`MySharedMem`, // Name of shared memory
(uint)sizeof(TestData), // Size of memory to allocate
0); // Flags
// Write pointers to class members
pAppData_ = pApp;
pSysData_ = pSys;
}
// [...]
您甚至可以在另一个类中创建 共享内存。重要的是,在创建回调时,有效的指针可用。
// [...]
public void createCallBack() {
int ksError;
// Fixed variable for temp. callback handle
Handle hCallBack;
CallBackRoutine callBack = new CallBackRoutine(callBackFunction);
if(pAppData_ != null) {
ksError = KS_createCallBack(
&hCallBack, callBack, pAppData_, KSF_USER_EXEC, 16);
// Write callback handle to class member
hCallBack_ = hCallBack;
// For example use it as timer callback
// [...]
}
}
每当调用回调函数时,指针都是有效的。因此,在删除回调之前,共享内存不会被释放。
public void removeCallBack() {
int ksError;
ksError = KS_removeCallBack(hCallBack_);
// Don't free shared memory while CallBack is active
ksError = KS_freeSharedMem(pAppData_);
}
}
同样,替代方案是 fixed语句,它不如共享内存灵活(见下文)。
public unsafe class Class2 : Kithara {
// [...]
public void f() {
int ksError;
Handle hEvent;
CallBackRoutine callBack = new CallBackRoutine(callBackFunction);
fixed (TestData* pData = &data_) {
ksError = KS_createCallBack(
&hEvent, callBack, pData, KSF_USER_EXEC, 16);
// For example use it as timer callback
// [...]
// If timer not needed anymore you can remove it
// But before leaving fixed context the callBack should be removed
ksError = KS_removeCallBack(hEvent);
// Now there is no copy of the pointer pData the Kithara driver
// could use
} // so we can leave fixed context
}
}
只要可以调用 callBackFunction,离开固定上下文将导致不可预测的程序行为,因为 data_ 是 Class2 的成员,因此是一个可移动的变量。因此,您首先必须使用KS_removeCallback
删除回调,然后离开固定上下文。
委托和 GC
GC 是否收集或重新分配我的委托,以便它们不使用 Kithara 创建回调?在应用程序中保留委托的有效句柄的同时,Kithara 驱动程序能够毫无问题地调用 回调函数 。封送委托时,Common语言Runtime (CLR) 确保与委托连接的 thunk 存储在非托管堆中,因此无法移动它。当 GC 未删除委托句柄时,thunk 将保持活动状态。有一篇德语 MSDN 文章涉及封送和 委托生存期。
编组
若要正确地将参数传递给 Kithara 函数,可能需要使用属性控制 .NET 封送器。在某些情况下,需要在要传递的参数的类型定义中声明属性。
例如,要将常量数组传递给非托管代码。对于编组器来说,使用给定数量的元素初始化数组是不够的。您必须告诉编组员阵列的大小。在类型定义中使用 MarshalAsAttribute:
// C#
public unsafe struct KSDeviceInfo{
[...]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)]
public char[] pDeviceName;
[...]
}
MarshallAsAttribute 的构造函数需要 UnmanagedType 枚举的值,该值是将参数传递给非托管代码的类型。
SizeConst 告诉编组员元素的计数。
从 DLL 导入如下所示:
// C#
[DllImport(`Kxxx.dll`, CallingConvention = CallingConvention.StdCall)]
public static extern unsafe int KS_getDeviceInfo(
string deviceName, ref KSDeviceInfo pDeviceInfo, int flags);
请注意,参数 pDeviceInfo 不是指针,而是由 ref 传递。
函数调用可以如下所示:
KSDeviceInfo deviceInfo = new KSDeviceInfo();
deviceInfo.structSize = (uint)Marshal.SizeOf(deviceInfo);
ksError = KS_getDeviceInfo(
devName, // Device name
ref deviceInfo, // KSDeviceInfo struct to write to by ref
0); // Flags, here 0
这是在KSDeviceInfo
中声明数组的结果。在 C# 中,不可能从引用类型获取指针,而数组是引用类型。因此,不可能从保存数组的结构中获取指针。因此,使用 ref 关键字代替将参数作为指针传递。因此,数据被编送到被叫方,然后编组回调用方。这意味着在函数返回后,指针对非托管代码无效。