问题描述

今天在做项目的时候碰到一个问题,就是用C++编写CLR类库dll的时候,C++的函数参数列表中包含一个char*的输出型参数,然而在C#调用该dll时候,会自动将函数的中的char*参数“翻译”为sbyte*, 使用了各种方法都不能调用函数,主要是不能合适的转换为sbyte*。

简单示例

举个简单的例子,比如我有一个CLR的类库为MyDll.dll, 其中头文件为Mydll.h,简单的只有一个函数,其中我想要为char*的作为输出参数, 代码如下

1 // MyDll.h
 2 
 3 #pragma once
 4 #include <cstdio>
 5 #include <cstring>
 6 using namespace System;
 7 
 8 namespace MyDll {
 9 
10     public ref class MyClass
11     {
12         // TODO:  在此处添加此类的方法。
13     public:
14         MyClass()
15         {}
16     public:
17         static bool TestFunc(char* outPutStr, int *p)
18         {
19             *p = 10;
20             char* s = "Hello world";
21             strcpy_s(outPutStr, 32, s);
22             return true;
23         }
24     };
25 }

源文件内容MyDll.cpp为空。【生成解决方案】后,得到MyDll.dll库。

1 // 这是主 DLL 文件, MyDll.cpp
2 
3 #include "stdafx.h"
4 
5 #include "MyDll.h"

然后新建一个C# WPF项目,右键 【引用】->【添加引用...】->左侧Tab【浏览】->右下角【浏览...】选择刚才生成的 MyDll.dll 文件后,点击【确定】。这样我们就可以在WPF项目中调用 TestFunc 函数了。

我们在 WPF 的 MainWidnow 中随便加入一个 Button,然后在 Button 的 Click 事件中尝试调用 TestFunc 函数,Mainwindow 的 cs 文件如下:

1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading.Tasks;
 6 using System.Windows;
 7 using System.Windows.Controls;
 8 using System.Windows.Data;
 9 using System.Windows.Documents;
10 using System.Windows.Input;
11 using System.Windows.Media;
12 using System.Windows.Media.Imaging;
13 using System.Windows.Navigation;
14 using System.Windows.Shapes;
15 using System.Runtime.InteropServices;
16 
17 namespace MyWPF
18 {
19     /// <summary>
20     /// MainWindow.xaml 的交互逻辑
21     /// </summary>
22     public partial class MainWindow : Window
23     {
24         public MainWindow()
25         {
26             InitializeComponent();
27         }
28 
29         private void Button_Click(object sender, RoutedEventArgs e)
30         {
31             unsafe
32             {
33                 string str = new string('A', 32);
34                 IntPtr intPtrStr = (IntPtr)Marshal.StringToHGlobalAnsi(str);
35                 sbyte* sbyteStr = (sbyte*)intPtrStr;
36 
37                 int p = 0;
38                 MyDll.MyClass.TestFunc(sbyteStr, &p);
39 
40                 String tmp = new String(sbyteStr);
41                 MessageBox.Show(tmp);
42                 MessageBox.Show(p.ToString());
43             }
44         }
45     }
46 }

注意引入第 15 行的命名空间, 同时【项目】->【属性】->【生成】->勾选 【允许不安全代码】。 主要转换的部分出现在 33-35 行,首先建立一个普通的 string 变量,然后新建一个指针,指向该 string的内容,再讲该指针强制类型转化为sbyte*, 从而实现从 char* 中获取内容。

问题讨论

问题1: 为什么 C# 会将 C++ 中的 char* 转换为 sbyte* 呢?

这是因为 C++ 中的 char 占用一个字节,而 C#中 char 占用两个字节,所以不能讲 char* 直接转换为 C# 中的 char*,  而 sbyte 是有符号单字节,取值区间 [-128, 127],所以 C# 将 char* 转换成 sbyte* 是合理的。

问题2:CLR 的 dll 和普通的 dll (Native dll) 有什么区别呢?

CLR 有点类似于 java 的 “虚拟机” , 提供运行环境,负责检查 内存管理、安全、异常等比较容易出错的问题,提高程序的安全性和健壮性,是 .NET Framework 的主要引擎。在 CLR 监视下的代码称为 “托管代码 (managed)”,  而其他的不在其监管下的代码称为 “非托管代码 (unmanaged)”。在 CLR 环境下生成的代码是中间代码,有点类似于 java 的字节码。所以通过 CLR Dll 生成的 dll 类库是中间代码,普通的 dll 则是直接是本机化的二进制文件, 通过 CLR DLL 生成的 dll 是程序集 (Assembly), 可以在直接在C#工程中【添加引用】就可以直接调用,从前面的例子也可以看出来,不需要任何的 DllImport 之类的包含与结构体重定义,函数指针定义等等复杂的操作,使得调用更加简单。