2015/4/26 技术探讨

   C#作为一门优秀的语言,其功能可谓居家旅行杀人放火必备的凶残语言,他有多强我也懒得介绍了,反正用过的都知道。但也有时候,我们需要某种特殊的功能,或许C#并没有提供,也或许出于某种保密性的需求,我们无法直接使用C#语言进行撰写,这就需要我们采用C#/Else Language混合编程,而本节内容中,我们将就如何让C为C#提供函数调用进行讲解。

    本节内容中,我们将探讨:

      (1)、写一个最简单的C函数并编译成库

      (2)、在C#中通过dllimport引入并调用C的库

      (3)、进一步研究两者之间的参数传递 


     一、写一个最简单的C函数并编译成动态库

    嗯,直入正题,我们先用GVIM写一个很简单的C函数:

    然后用GCC对它进行编译(注意:这里是使用Windows中的GCC,采用MinGW安装,如有不懂如何使用的读者,可以移步到《如何安装使用MinGW》进行学习)。

    然后就出现了一个编译好的库,我们C这边的工作就基本完成了。

 

    二、如何通过Dllimport引入C写的库

    如何引入一个非C#的库,通常的方法就是采用DllImport,通过P/Invoke机制进行引入。当然在Mono上除了使用Dllimport外,还有另外的方式引入,这里我们不作任何探讨。

    我们新建一个控制台程序,然后写上我们的程序:

 1 namespace demo2
 2 {
 3     internal class Program
 4     {
 5         private static void Main(string[] args)
 6         {
 7             var cLib = new CLib();
 8             var p = CLib.SayHello();
 9             var str = Marshal.PtrToStringAuto(p);
10             Console.WriteLine(str);
11             Console.ReadKey();
12         }
13     }
14 
15     internal class CLib
16     {
17         [DllImport("你的so文件路径/demo1.so")]
18         public extern static IntPtr SayHello();
19     }
20 }

     按下F6编译之后,我们在运行编译好的exe文件:

    瞧,成功调用了C的函数了,SO Easy,这里要注意一点的,也是很多童鞋经常会犯的错误,那就是我们需要找到控制台bin里的exe,双击运行,不能直接在VS中按F5调试运行,否则是无法看到C语言输出的东西的。

 

    三、关于参数传递

    关于C/C#之间的参数传递,这里水比较深,抛开数组、结构体等复杂类型不说,就简单类型(int、char等)而言我私以为可以分为两个部分,其一就是C#向C语言的参数传递,另外就是C语言向C#的return。

    我们先对C#->C的传递方式进行讲解,同样的我们也继续上demo。

    我们定义了一个计算加法并输出的函数,然后修改我们C#的源代码为:

点我看源码
复制代码
 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 namespace demo2
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var a = 10;
11             var b = 20;
12             var cLib = new CLib();
13             unsafe
14             {
15                 CLib.Add(&a, &b);
16             }
17             Console.ReadKey();
18         }
19     }
20 
21     internal class CLib
22     {
23         [DllImport("你的路径/c/demo1.so")]
24         public unsafe extern static IntPtr Add(int* a,int* b);
25     }
26 }
复制代码
按 Ctrl+C 复制代码
按 Ctrl+C 复制代码

     按下F6之后运行exe文件:

     Oh~Year。同样没有问题。

    我们在此基础上,试试字符串,同样的,我们在C语言这里添加一个新函数:

    同样的C#这里也进行改造:

戳我戳我
复制代码
 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 namespace demo2
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var str = "你好,我是小蝶惊鸿";
11 
12             unsafe
13             {
14                 fixed (char* p = str)
15                 {
16                     CLib.Say(p);
17                 }
18             }
19             Console.ReadKey();
20         }
21     }
22 
23     internal class CLib
24     {
25         [DllImport("你的地址/demo1.so")]
26         public unsafe extern static IntPtr Say(char* input);
27     }
28 
复制代码
按 Ctrl+C 复制代码
按 Ctrl+C 复制代码

     然后再重新生成so文件,重新编译C#,并点击exe运行:

 

    可以看出,程序立马报了个错误,具体原因就不跟各位读者探讨了,大概就是字符串没有结束符,造成printf读取完字符串本身之后还继续的读其他内存,造成了越界。我们把代码小改一下,由外部传入一个字符串的长度。这里还需要注意一点,那就是在C语言中,char只占一个字节而C#中的char则是两个字节,由C#传入的字符串还需要转换一下。修改后的代码如下图所示:

    同样的C#代码也跟着修改: 

快戳快戳
复制代码
 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 namespace demo2
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var str = "hi,i am xiaodiejinghong";
11 
12             unsafe
13             {
14                 fixed (char* p = str)
15                 {
16                     int length = str.Length;
17                     CLib.Say(p, &length);
18                 }
19             }
20             Console.ReadKey();
21         }
22     }
23 
24     internal class CLib
25     {
26         [DllImport("E:/ASP/Mono/project/嵌入技术/c/demo1.so", CallingConvention = CallingConvention.StdCall)]
27         public unsafe extern static IntPtr Say(char* input, int* length);
28     }
29 }
复制代码
按 Ctrl+C 复制代码
按 Ctrl+C 复制代码

      编译后再次运行:

    程序正常无误。至此,C#向C传递参数部分暂且讲解完毕,下面我们再讲解C#如何接收从C函数返回的信息。

    可能有读者认为直接使用Return即可,在某种程度上,确实是可以使用Return,譬如返回一个在C中写死了的字符串(那是因为编译器已经把这段固定字串当成常量存储起来),但如若需要返回一串动态的字串,这种直接Return的方式就行不通了(字符串离开函数之后被回收,不信可以试试)。我们需要使用一些其他方法来接收从C返回的资源,在这里,我们要给各位读者介绍的方式是通过在C#中给出一个容器,并把它的指针传入C中,C需要返回的东西都存放到改容器中,这样想返回的东西就不会被C回收掉了。

    我们的C示例代码如下:

    C#的代码如下: 

戳我展开看源码
复制代码
 1 using System;
 2 using System.Runtime.InteropServices;
 3 
 4 namespace demo2
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var str1 = "hi,";
11             var str2 = "i am xiaodiejinghong ";
12             var l1 = str1.Length;
13             var l2 = str2.Length;
14             var output = new char[l1 + l2];
15             unsafe
16             {
17                 fixed (char* p1 = str1) fixed (char* p2 = str2) fixed (char* op = output)
18                 {
19                     CLib.Merge(p1, &l1, p2, &l2, op);
20                     Console.WriteLine(Marshal.PtrToStringAnsi((IntPtr)op));
21                 }
22             }
23             Console.ReadKey();
24         }
25     }
26 
27     internal class CLib
28     {
29         [DllImport("你的路径/demo1.so", CallingConvention = CallingConvention.StdCall)]
30