一、问题描述

项目要求调用某公司的DLL链接库操作LED设备,要求使用WPF开发该应用程序。链接库为pArmSendQt.dll ,操作为:

【函数格式】

int SS_Send_Power_On (void)

【函数参数】

【函数返回值】

成功:0

失败:返回错误码代码

项目开始,应用程序与链接库调用程序分开开发,为了方便,链接库调用程序使用控制台应用程序开发,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void Main(string[] args)
{
int i;
i = test.SS_Send_Power_Off();
Console.WriteLine(m.errorlist[i]);
Console.WriteLine("关闭通讯连接");
Console.ReadLine();
}
class test
{
[DllImport("..\..\lib\pArmSendQt.dll")]
public static extern int SS_Send_Power_Off();
}

运行无任何问题,然后进入合并阶段——将wpf和该调用程序合并。以为很顺利的就完成任务,结果……开始了漫长的debug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
int i;
i = test.SS_Send_Power_On();
listBox1.Items.Add(i);
}
}
class test
{
[DllImport("pArmSendQt.dll", EntryPoint = "SS_Send_Power_On")]
public static extern int SS_Send_Power_On();
}
```
运行即出现
![](../images/pic/other_site/img_my_1333076814_4495.jpg)
提示为堆栈溢出,可能是出现死循环或者无限递归。
可是这个程序里那里有循环可言呢?
开始逐行调试:
发现程序没有任何问题,只是调试到button1事件结束后,弹出该错误,没有任何代码跟踪提示!
**二、debug过程**
1.由于逐行调试,没有发现问题所在,而最初使用的控制台程序却没有问题,所以首先把问题引到了项目类型,于是,使用winform操作一番:
```csharp
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
int i;
i = test.SS_Send_Power_On();
listBox1.Items.Add(i);
}
}
class test
{
[DllImport("pArmSendQt.dll")]
public static extern int SS_Send_Power_On();
}

居然发现程序没有任何错误,每一次button1都能顺利将调用函数的返回值添加到listbox中。难道是因为wpf和winform控件的问题么?

2.为了确认或排除上一步的问题,我将winform控件引入到wpf中

然后对winform的listbox控件进行操作,发现也出现错误。那就排除了第一个可能。在不停调试中,无意用label控件来记录函数返回值,发现没有错误弹出了,难道是listbox控件引发的问题?

3.分别将函数返回值放到label,textbox中,发现都没有问题,尝试用messagebox也没有问题了。真的是listbox的问题么?那为什么只有listbox会触发这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int i;
i = test.SS_Send_Power_On();
//listBox1.Items.Add(i);
textbox1.Text += i;
label1.Content = i.toString();
MessageBox.Show(i.toString());
```
运行几次程序后,发现问题没这么“简单”,一眼看上去没有错误弹出了,但点击窗口上任意一个控件,都会触发那个错误。也就是说,只要调用dll函数后,点击控件就会出现错误。这也解释了为什么listbox会自动触发该错误的原因了:因为listbox的add方法自动触发了SelectChange方法,而后面的控件通过点击等操作也触发了相应的方法。所以得出一个猜测结论:**在WPF中调用C/C++
DLL后,触发任何一个控件方法,都会弹出堆栈溢出的错误。**
4.这个猜测有没有较好的根据,此时还没法说明。但仔细考虑一番,MS会允许这样大一个问题出现在WPF中么?会不会是DLL本身的问题,其实这个才应该是最先考虑并确认的,只是第一个控制台程序的成功运行麻痹了人的思想。回过头来,解决这个问题:
从网上搜索了几个c/c++的DLL,调用运行一番,发现有的不能运行,有的没有问题,有的弹出相同的错误。这个结果很不理想,因为并没有说明该问题是否由DLL本身引出。同时由于没有自己编写DLL的经验,不能确认猜测是否准确。于是这个计划破产。
5.综合了种种想法,最后给出一个猜测:**DLL本身可能对程序造成了一定的影响,同时,WPF的某些机制与winform不同,这个机制也是引起问题可疑因素。**不过由于我自身也是初学WPF,对WPF没有深入的了解,所以没法分析出究竟什么样的机制产生了这个差异。但有一点我比较清楚:WPF的消息与线程机制与winform不同,这给我刚开始从winform转WPF开发带来了不小的麻烦。于是大胆假设一番,从我较熟悉的地方入手。
6.这时候,搜寻资料就比较有目的性了:“WPF调用DLL产生溢出”,“WPF与Winform调用DLL“等关键词作为搜索目标。果然,在众多结果中找到了[点击打开链接](http://social.msdn.microsoft.com/Forums/zh-SG/wpfzhchs/thread/16896fba-355b-4765-a32e-bee2845b4528)。
我WPF (.net4.0)下,调用C++的DLL的方法 ,这个DLL的方法是:
```csharp
//蜂吟
[DllImport("mi.dll", EntryPoint = "API_ControlBuzzer", CharSet = CharSet.Auto, CallingConvention
= CallingConvention.Winapi)]
public static extern int API_ControlBuzzer(int hComm, int DeviceAddress, int time, int count,
byte[] buf);

其作用是访问读卡器上的一个蜂吟器发声,我是把这段代码放到一个类AceReader里,

调用方式:AceReader.API_ControlBuzzer(0,0,10,1,buf)

现在的问题是调用完成时很正常,完成后,我在wpf点击其它任何按钮的动作或打开窗口等等commond动作时都报错:System.StackOverflowException

为此,我请一朋友用C++写了一个测试 a.DLL,里面有一个方法:

1
2
3
[DllImport("a.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
public static extern void MyBox(int iNum);

这个方法的作用是弹出一个提示信息,没有访问硬件,这样就没有问题

其本上可以认为:访问硬件后,就会出现System.StackOverflowException

关于上面提到的C++dll : “mi.dll”

我在winForm (.net4.0) 下作同样的测试,都很正常。也就是winform上一切正常,没有问题

我在WPF(.net4.0) 一旦访问了”mi.dll”里的访问硬件的相关方法,访问完成后,再做其它动作时就会出现System.StackOverflowException

是什么原因?

这个描述与我遇到的问题完全一致,在问题解答中,我也找到了答案:

由于WPF和以前的Winform使用了不同的消息机制和线程机制,所以在你的dll中抛出的大量异常都被推到了Window的Frames中去了,导致溢出。对于这个问题,你只有尝试去从你的C++ Dll 入手去改了,在你的C++ dll中一定要把异常处理好,线程要做到和被PInvoke调用的代码独立。否则,Native代码被调用到了我们的主线程中,他甚至可以在主线程修改值,会导致我们的WPF 线程中Frame无法受到 WPF 的 Dispatcher 控制。

同时,提供了一个临时的解决办法:调把用代码放入另外一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() => { AceReader.API_ControlBuzzer(10, 1); });
thread.Start();
}
private void button2_Click(object sender, RoutedEventArgs e)
{
Window1 w1 = new Window1();
w1.Show();
}

由于手头只有封装好的DLL,所以只好使用这个临时方法。

1
2
3
4
5
6
7
8
9
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() =>
{
i = test.SS_Send_Power_On();
listBox1.Items.Add(i);
});
thread.Start()
}

这段代码的问题相信大家一眼就能看出了,就是控件跨越了线程

这个问题相对来说就好解决了,可以参考一下点击打开链接,我这里为了省事,将代码这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
private void button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(() =>
{
i = test.SS_Send_Power_On();
});
thread.Start();
Thread.Sleep(100);
listBox1.Items.Add(i);
}

终于,问题算是解决了

三、总结

该问题确实由DLL与WPF消息与线程共同引起的,在编写DLL时需要按照严格规范处理好异常。如果是引用他人DLL开发,可以将调用函数放在另一个线程中,避免和主线程冲突。同时,该问题也反映出我对于WPF的了解甚少,基础不扎实,还需要进一步学习,对于分析问题方面,思维应该更敏捷,眼光应该更敏锐。

DLL下载:http://115.com/file/dpzcx057 由于商业用途,未经公司许可,不能把DLL操作放出,只提供文章中例子的操作,望见谅。