zoukankan      html  css  js  c++  java
  • python与c混和编程

    python扩展实现方法--python与c混和编程

     

    前言

    大部分的Python的扩展都是用C语言写的,但也很容易移植到C++中。
    一般来说,所有能被整合或者导入到其它python脚本的代码,都可以称为扩展。
    扩展可以用纯Python来写,也可以用C或者C++之类的编译型的语言来扩展。
     
    就算是相同的架构的两台电脑之间最好也不要互相共享二进制文件,最好在各自的
    电脑上编译Python和扩展。因为就算是编译器或者CPU之间的些许差异。
     
    官方文档
     
     

    需要扩展Python语言的理由:

    1. 添加/额外的(非Python)功能,提供Python核心功能中没有提供的部分,比如创建新的
    数据类型或者将Python嵌入到其它已经存在的应用程序中,则必须编译。
     
     
    2. 性能瓶颈的效率提升, 解释型语言一般比编译型语言慢,想要提高性能,全部改写成编译型
    语言并不划算,好的做法是,先做性能测试,找出性能瓶颈部分,然后把瓶颈部分在扩展中实现,
    是一个比较简单有效的做法。
     
     
    3. 保持专有源代码的私密,脚本语言一个共同的缺陷是,都是执行的源代码,保密性便没有了。
    把一部分的代码从Python转到编译语言就可以保持专有源代码私密性。不容易被反向工程,对涉及
    到特殊算法,加密方法,以及软件安全时,这样做就显得很重要。
     
     
    另一种对代码保密的方式是只发布预编译后的.pyc文件,是一种折中的方法。
     
     

    创建Python扩展的步骤

    1. 创建应用程序代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    #define BUFSIZE 10

    int fac(int n) {
        if (n < 2)
            return 1;
        return n * fac(n - 1);
    }

    char *reverse(char *s) {
        register char t;
        char *= s;
        char *= (s + (strlen(s) - 1));
        while (p < q) {
            t = *p;
            *p++ = *q;
            *q-- = t;
        }
        return s;
    }

    int main() {
        char s[BUFSIZE];
        printf("4! == %d\n", fac(4));
        printf("8! == %d\n", fac(8));
        printf("12! == %d\n", fac(12));
        strcpy(s, "abcdef");
        printf("reversing 'abcdef', we get '%s'\n", reverse(s));
        strcpy(s, "madam");
        printf("reversing 'madam', we get '%s'\n", reverse(s));
        return 0;
    }
    一般是需要写main()函数,用于单元测试
     
    使用gcc进行编译
    >gcc Extest.c -o Extest
    执行
    >./Extest
     

    2. 利用样板来包装代码

    整个扩展的实现都是围绕"包装"这个概念来进行的。你的设计要尽可能让你的实现语言与Python无缝结合。
    接口的代码又被称为"样板"代码,它是你的代码与Python解释器之间进行交互所必不可少的部分:
    我们的样板代码分为4步:
    a. 包含python的头文件
    需要找到python的头文件在哪,一般是在/usr/local/include/python2.x中
    在上面的C代码中加入#include "Python.h"
     
     
    b. 为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数
    包装函数的用处就是先把python的值传递给c,再把c中函数的计算结果转换成Python对象返回给python。
    需要为所有想被Python环境访问到的函数都增加一个静态函数,返回类型为PyObject *,函数名格式为
    模块名_函数名;
    static PyObject * Extest_fac(PyObject *self, PyObject *args) {
        int res;//计算结果值
        int num;//参数
        PyObject* retval;//返回值

        //i表示需要传递进来的参数类型为整型,如果是,就赋值给num,如果不是,返回NULL;
        res = PyArg_ParseTuple(args, "i"&num); 
        if (!res) {
            //包装函数返回NULL,就会在Python调用中产生一个TypeError的异常
            return NULL;
        }
        res = fac(num);
        //需要把c中计算的结果转成python对象,i代表整数对象类型。
        retval = (PyObject *)Py_BuildValue("i", res);
        return retval;
    }
    也可以写成更简短,可读性更强的形式:
    static PyObject * Extest_fac(PyObject *self, PyObject *args) {
        int m;
        if (!(PyArg_ParseTuple(args, "i"&num))) {
            return NULL;
        }
        return (PyObject *)Py_BuildValue("i", fac(num));
    }
    下面是python和c对应的类型转换参数表:
    这里还有一个Py_BuildValue的用法表:
     
    reverse函数的包装也类似:
    static PyObject *
    Extest_reverse(PyObject *self, PyObject *args) {
        char *orignal;
        if (!(PyArg_ParseTuple(args, "s"&orignal))) {
            return NULL;
        }
        return (PyObject *)Py_BuildValue("s", reverse(orignal));
    }
    也可以再改造成返回包含原始字串和反转字串的tuple的函数
    static PyObject *
    Extest_doppel(PyObject *self, PyObject *args) {
        char *orignal;
        if (!(PyArg_ParseTuple(args, "s"&orignal))) {
            return NULL;
        }
        //ss,就可以返回两个字符串,应该reverse是在原字符串上进行操作,所以需要先strdup复制一下
        return (PyObject *)Py_BuildValue("ss", orignal, reverse(strdup(orignal)));
    }
    上面的代码有什么问题呢?
    和c语言相关的问题,比较常见的就是内存泄露。。。上面的例子中,Py_BuildValue()函数生成
    要返回Python对象的时候,会把转入的数据复制一份。上面的两个字符串都被复制出来。但是
    我们申请了用于存放第二个字符串的内存,在退出的时候没有释放掉它。于是内存就泄露了。
     
    正确的做法是:先生成返回的python对象,然后释放在包装函数中申请的内存。
    static PyObject *
    Extest_doppel(PyObject *self, PyObject *args) {
        char *orignal;
        char *reversed;
        PyObject * retval;
        if (!(PyArg_ParseTuple(args, "s"&orignal))) {
            return NULL;
        }
        retval = (PyObject *)Py_BuildValue("ss", orignal, reversed=reverse(strdup(orignal)));
        free(reversed);
        return retval;
    }
     
    c. 为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组
    我们已经创建了几个包装函数,需要在某个地方把它们列出来,以便python解释器能够导入并调用它们。
    这个就是ModuleMethods[]数组所需要做的事情。
    格式如下 ,每一个数组都包含一个函数的信息,最后一个数组放置两个NULL值,代表声明结束
    static PyMethodDef 
    ExtestMethods[] = {
        {"fac", Extest_fac, METH_VARARGS}, 
        {"doppel", Extest_doppel, METH_VARARGS},
        {"reverse", Extest_reverse, METH_VARARGS},
        {NULL, NULL},
    };
    METH_VARARGS代表参数以tuple的形式传入。如果我们需要使用PyArg_ParseTupleAndKeywords()
    函数来分析关键字参数的话,这个标志常量应该写成: METH_VARARGS & METH_KEYWORDS,进行逻辑与运算。
     
     
    d. 增加模块初始化函数void initMethod()
    最后的工作就是模块的初始化工作。这部分代码在模块被python导入时进行调用。
    void initExtest() {
        Py_InitModule("Extest", ExtestMethods);
    }
     
    最终代码如下:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "Python.h"

    #define BUFSIZE 10

    int fac(int n) {
        if (n < 2)
            return 1;
        return n * fac(n - 1);
    }

    char *reverse(char *s) {
        register char t;
        char *= s;
        char *= (s + (strlen(s) - 1));
        while (p < q) {
            t = *p;
           *p++ = *q;
           *q-- = t;
        }
        return s;
    }

    static PyObject *
    Extest_fac(PyObject *self, PyObject *args) {
        int res;
        int num;
        PyObject* retval;

        res = PyArg_ParseTuple(args, "i"&num);
        if (!res) {
            return NULL;
        }
        res = fac(num);
        retval = (PyObject *)Py_BuildValue("i", res);
        return retval;
    }

    static PyObject *
    Extest_reverse(PyObject *self, PyObject *args) {
        char *orignal;
        if (!(PyArg_ParseTuple(args, "s"&orignal))) {
            return NULL;
        }
        return (PyObject *)Py_BuildValue("s", reverse(orignal));
    }

    static PyObject *
    Extest_doppel(PyObject *self, PyObject *args) {
        char *orignal;
        char *resv;
        PyObject *retval;
        if (!(PyArg_ParseTuple(args, "s"&orignal))) {
            return NULL;
        }
        retval = (PyObject *)Py_BuildValue("ss", orignal, resv=reverse(strdup(orignal)));
        free(resv);
        return retval;
    }

    static PyMethodDef 
    ExtestMethods[] = {
        {"fac", Extest_fac, METH_VARARGS},
        {"doppel", Extest_doppel, METH_VARARGS},
        {"reverse", Extest_reverse, METH_VARARGS},
        {NULL, NULL},
    };

    void initExtest() {
        Py_InitModule("Extest", ExtestMethods);
    }

    int main() {
        char s[BUFSIZE];
        printf("4! == %d\n", fac(4));
        printf("8! == %d\n", fac(8));
        printf("12! == %d\n", fac(12));
        strcpy(s, "abcdef");
        printf("reversing 'abcdef', we get '%s'\n", reverse(s));
        strcpy(s, "madam");
        printf("reversing 'madam', we get '%s'\n", reverse(s));
        test();
        return 0;
    }
     
     

    3. 编译与测试

    为了让你的新python扩展能够被创建,你需要把它们与python库放在一起编译。python中的distutils包被
    用来编译,安装和分发这些模块,扩展和包。步骤如下:
    a. 创建setup.py
    我们在安装python第三方包的时候,很多情况下会用到python setup.py install这个命令,
    下面我们来了解一下setup.py文件的内容。
     
    编译的最主要的内容由setup函数完成,你需要为每一个扩展创建一个Extension实例,在这里我们只有一个
    扩展,所以只需要创建一个实例。
    Extension('Extest', sources=['Extest.c']),第一个参数是扩展的名字,如果模块是包的一部分,还需要加".";
    第二个参数是源代码文件列表
    setup('Extest', ext_modules=[...]),第一个参数表示要编译哪个东西,第二个参数列出要编译的Extension对象。
    #!/usr/bin/env python
    from distutils.core import setup, Extension
    MOD = 'Extest'
    setup(name=MOD, ext_modules=[Extension(MOD, sources['Extest.c'])])
    setup函数还有很多选项可以设置。详情可见官网。
     
     
    b. 通过运行setup.py来编译和连接你的代码
    在shell中运行命令
    >python setup.py build
    当你报错如:无法找到Python.h文件
    那么说明你没有安装python-dev包,需要去官网下载源码包重装自己编译安装一下python。
    Python.h文件一般会出现在/usr/include/Python2.X文件夹中,我这里反正是没有的。。。
    只有重新编译一个python...
     
    我现在linux系统上的python版本是2.6.6,我下载一个相同版本的源码,也可以下载更高版本。
     
    解压源码包
    > tar xzf Python-2.6.6.tgz
    > cd Python-2.6.6.tgz
    编译安装Python
    > ./configure --prefix=/usr/local/python2.6
    > make
    > sudo make install
    创建一个新编译python的链接
    > sudo ln -sf /usr/local/python2.6/bin/python2.6 /usr/bin/python2.6
    测试一下,可用
    使用这种方法可以在Linux上运行不同版本的python.
     
    Python.h文件也在/usr/local/python2.6/include/python2.6路径下找到。
    重新运行编译
     
    编译成功后,你的扩展就会被创建在bulid/lib.*目录下。你会看到一个.so文件,这是linux下的
    动态库文件:
     
    c. 进行调试
    你可以直接用python代码调用进行测试:
    #!/usr/bin/python
    from ctypes import *
    import os 
    #需要使用绝对路径
    extest = cdll.LoadLibrary(os.getcwd() +'/Extest.so'
    print extest.fac(4)
     
    也可以在当前目录下执行命令,安装到你的python路径下,这种就挺像安装第三方插件的样子
    > python setup.py install
    安装成功的话,直接导入测试:
     
    最后需要注意一点的是,原来的c文件中有一个main函数,因为一个系统中只能有一个main
    函数,所以为了不起冲突,可以把main函数改成test函数,再用Extest_test()包装函数处理一下,
    再加入ExtestMethods数组,这样就可以调用这个测试函数了。
    static PyObject *
    Extest_test(PyObject *self, PyObject *args) {
        test();
        #返回空的话,就使用下面这一句 
        return (PyObject *)Py_BuildValue("");
    }



    摘要: 前言需要扩展Python语言的理由:创建Python扩展的步骤1. 创建应用程序代码2. 利用样板来包装代码a. 包含python的头文件b. 为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数c. 为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组 d. 增加模块初始化函数void initMethod()3. 编译与测试a. 创建setup.pyb. 通过运行setup.py来编译和连接你的代码 c. 进行调试前言大部分的Python的扩展都是用C语言写的,但也很容易移植到C++中。一般来说,所有能被整合或者导入到其它阅读全文
    posted @ 2012-09-04 19:06 btchenguang 阅读(582) | 评论 (0)编辑
     
    摘要: HTML代码自动转义(auto-escaping)当使用模板生成HTML代码时,如果变量内容是一些影响HTML结果的字符时,那就挺危险的。例如,模板内容如下:Hello {{ name }}当name的值为:<script>alert('hello')</script>渲染后的HTML结果就是:Hello <script>alert('hello')</script>以上的代码运行的结果就是会让浏览器弹出一个javascript的警告窗口。同理,如果name的值为<b>hanks,那么结果中Hello以阅读全文
    posted @ 2012-09-03 16:49 btchenguang 阅读(680) | 评论 (2)编辑
     
    摘要: 也许,你想要自定义和扩展模板引擎,下面会介绍一些关于如何去扩展模板系统的方法,了解一下模板系统的工作原理,同时也会介绍Django模板系统中的auto-escapint功能,这是一种安全机制。复习一下模板语言的用法{# 模板tag的用法 #}{% if done %}<strong>Over</strong>{% else %}<strong>wait</strong>{% endif %}{# 模板变量的用法 #}Now is {{ nowtime }}在views.py中使用模板的时候:1. 通过模板名,获得模板对象2. 创建context对阅读全文
    posted @ 2012-09-01 16:06 btchenguang 阅读(544) | 评论 (0)编辑
     
    摘要: 原贴来源http://wiki.woodpecker.org.cn/moin/ObpLovelyPython/AbtWebModulespython实现web服务器web开发首先要有web服务器才行。比如apache,但是在开发阶段最好有一个简单方便的开发服务器,容易重启进行调试,等开发调试完毕后,再将代码部署到成熟稳定高效的web服务器。# -*- coding: utf-8 -*-from wsgiref import simple_server# 定义一个输出 hello world 和环境变量的简单web应用程序def hello_app(environ, start_response阅读全文
    posted @ 2012-08-31 10:00 btchenguang 阅读(427) | 评论 (4)编辑
     
    摘要: 可调用的对象是python执行环境中最重要的部分,python语句,赋值,表达式,模块等,这些对象只是构成可执行代码块的拼图的很少的一部分,而这些代码块被称为代码对象。每个可调用的对象的核心都是代码对象。一般来说,代码对象可以作为函数或者方法调用的一部分来执行,也可以用exec语句或者是内建函数eval()来执行。从整体上来看,一个python模块的代码对象,是构成该模块的全部代码。如果要执行python代码,那么该代码必须先要转换成字节编译的代码,这才是真正的代码对象。然而,它们不包含任何关于它们执行环境的信息,这便是可调用物存在的原因,它被用来包装一个代码对象并提供额外的信息。udf.fu阅读全文
    posted @ 2012-08-29 19:19 btchenguang 阅读(585) | 评论 (0)编辑
     
    摘要: Admin后台管理模块的使用Django的管理员模块是Django的标准库django.contrib的一部分。这个包还包括其它一些实用的模块:django.contrib.authdjango.contrib.sessionsdjango.contrib.comments激活admin模块的方法是:1. 在INSTALLED_APPS设置文件中,加入'django.contrib.admin'2. 再添加'django.contrib.auth','django.contrib.contenttypes''django.contrib.阅读全文
    posted @ 2012-08-29 17:20 btchenguang 阅读(86) | 评论 (0) 编辑
     
    摘要: 模板使用模板基本由两个部分组成,一是HTML代码,二是逻辑控制代码。逻辑控制的实现又基本由三个部分组成:1. 变量的使用{{ person_name }} #使用双大括号来引用变量2. tag的使用{% if ordered_warranty %} #使用大括号和百分号的组成来表示使用Django提供的template tag{% for item in item_list %}<li>{{ item }}</li>{% endfor %}3. filter的使用{{ ship_date|date:"F j, Y" }},ship_date变量传给d阅读全文
    posted @ 2012-08-29 17:18 btchenguang 阅读(61) | 评论 (0) 编辑
     
    摘要: 第一个django程序1. 创建一个django工程python D:\Python27\Lib\site-packages\django\bin\django-admin.py startproject helloworld目前windows下我只发现用这样的命令可以成功创建project。。。这样就会在当前目录下创建一个helloworld工程文件夹。2. 创建views.py文件在工程文件夹根目录创建views.py文件,其实任意文件名都可以,使用views是为了遵循传统。在里面输入:from django.http import HttpResponse#所有的views函数都需要以r阅读全文
    posted @ 2012-08-29 17:17 btchenguang 阅读(26) | 评论 (0) 编辑
     
    摘要: 初识Django自称MTV框架。和传统的MVC大同小异。M指数据模型(Model),T指模板(Template),用来描述数据的展现。V指的是视图(View),并且,在Django中可以通过URL分发器对URL和View之间的映射进行配置,而View则URL分发器回调。Django发布在2005年7月,为了纪念法国爵士吉它手Django Reinhardt快速体验首先,官网下载,安装,使用命令python setup.py install验证安装成功与否,可以输入:import django看是否报错使用django命令创建一个工程文件夹目前只能输入:python D:\Python27\Li阅读全文
    posted @ 2012-08-29 17:17 btchenguang 阅读(146) | 评论 (4)编辑
     
    摘要: Views,URLconf的advanced用法之前有介绍了一些views和路径匹配的基础用法,在这里介绍一些关于它们的advanced用法。URLconf技巧因为urls.py也是一个python文件,所以你可以在这个文件中使用python允许的任何语法。先看之前介绍的例子:from django.conf.urls import patterns, include, urlfrom books.views import hello, search_form, search, contact, thanksurlpatterns = patterns('',url(r'阅读全文
    posted @ 2012-08-28 20:34 btchenguang 阅读(607) | 评论 (0)编辑
     
    摘要: 元类MetaClass元类是可以让你定义某些类是如何被创建的。从根本上说,赋予你如何创建类的控制权。元类也是一个类,是一个type类。元类一般用于创建类。在执行类定义时,解释器必须要知道这个类的正确的元类,如果此属性没有定义,它会向上查找父类中的__metaclass__属性。如果还没发现,就查找全局变量。对于传统类来说,它们的元类是types.ClassType。元类也有构造器,传递三个参数:类名,从基类继承数据的元组,和类属性字典下面我们来定义一个元类,要求写类的时候必须给类提供一个__str__()方法,如果没有提供__repr__()方法,则给你警告。from warnings imp阅读全文
    posted @ 2012-08-27 23:19 btchenguang 阅读(795) | 评论 (2)编辑
     
    摘要: FormsHTML form是交互网页的支柱。下面来学习一下有关使用Django处理用户提交的表单数据,验证等功能。我们将讨论HttpRequest和Form对象。request中包含的信息在views.py中的每一个用于显示页面的函数都需要以request作为第一个函数参数。request包含了一些有用的信息,如:request.path 除去了域名和端口的访问路径,request.get_host 域名+端口信息request.get_full_path() 所有路径,包含传递的参数requets.is_secure() 是否使用https进行链接还有一个特别的属性request.META阅读全文
    posted @ 2012-08-27 15:06 btchenguang 阅读(774) | 评论 (2)编辑
     
    摘要: Model使用首先安装MySQL的python连接驱动,windows下安装可下下载,对应python-2.7:https://code.google.com/p/soemin/downloads/detail?name=MySQL-python-1.2.3.win32-py2.7.exe&can=2&q=简单的,先展示在view中使用mysql数据库操作from django.shortcuts import render_to_responseimport MySQLdbdef book_list(request): db = MySQLdb.connect(user=&#阅读全文
    posted @ 2012-08-25 14:22 btchenguang 阅读(999) | 评论 (3)编辑
     
    摘要: 问题描述: 随机给出一串数i, 要能够给出其中大小中间的那个数 算法描述: 一般做法,做插入排序,然后中间值在索引一半的位置,时间复杂度一般,插入排序平均时间复杂度O(n2),再找中间 值,效率不高。 这里的做法是,引入数据结构--Heap来解决问题,时间复杂度为O(logn)。 引入两个堆,max heap和 min heap来存放整数串i的两个部分,需要满足如下条...阅读全文
    posted @ 2012-05-06 10:31 btchenguang 阅读(47) | 评论 (0) 编辑
     
    摘要: 有问题,调试了很久也没有发现错在哪里。。。高手们,请指教源码附件:https://files.cnblogs.com/btchenguang/scc.zip概念:有向图中的强连通指的是可以相互访问到的顶点的集合,简而言之是组成环的顶点的集合,在一个有向图中可能有很多个不同的强连通部分算法思想:1. Let G be a directed graph and S be an empty stack.2. While S does not contain all vertices Choose an arbitrary vertex v not in S. Perform a depth-first阅读全文
    posted @ 2012-04-26 16:05 btchenguang 阅读(62) | 评论 (0) 编辑
     
    摘要: 想了解一下python的性能调试方法,结果就看到这一篇文章,想翻译下来作个记录 原文来自于:http://docs.python.org/library/profile.html?highlight=profile#cProfile 1. 介绍性能分析器 profiler是一个程序,用来描述运行时的程序性能,并且从不同方面提供统计数据加以表述。Python中含有3个模块提供这样的功能,分别是...阅读全文
    posted @ 2012-02-03 15:03 btchenguang 阅读(266) | 评论 (0)编辑
     
    摘要: 需要在程序中使用二维数组,网上找到一种这样的用法: #创建一个宽度为3,高度为4的数组#[[0,0,0], # [0,0,0],# [0,0,0],# [0,0,0]]myList = [[0] * 3] * 4但是当操作myList[0][1] = 1时,发现整个第二列都被赋值,变成[[0,1,0], [0,1,0], [0,1,0], [0,1,0]] 为什么...一时搞不懂,后面翻阅The...阅读全文
    posted @ 2012-01-30 22:38 btchenguang 阅读(2041) | 评论 (2)编辑
     
    摘要: DP问题的特征:重复子问题存在最优子集背包问题属于经典的DP问题,而0/1背包问题是属于最简单的情况。0/1的意思是每种物品只有一件,要么放入背包中,要么不放问题定义:有N个物品,要放入容量为W的背包中,第i件物品重量为w(i),价值为v(i),问要怎样放才能在不超过背包容量的基础上,获得最大的价值。算法描述:需要用到递归的思想,定义A(i, j)为前i个物品中在容量为j的情况下所能达到的最大价值,则A(0,j) = 0,A(i,0) = 0(i <= N and j <= W).如果w(i) > j时,说明第i件物品不能放入背包中,价值不变,所以A(i, j) = A(i 阅读全文
    posted @ 2012-01-18 14:40 btchenguang 阅读(88) | 评论 (0) 编辑
     
    摘要: 1. 需要把example.py文件所在的文件夹路径添加到系统path环境变量中2. 在调用的时候>>>import example>>>example.function()python编译器首先是在当前路径(python.exe所在文件夹)下寻找文件,然后再去path定义的路径中寻找文件阅读全文
    posted @ 2012-01-18 13:58 btchenguang 阅读(141) | 评论 (0)编辑
    作者:btchenguang
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
    分类: python
    标签: python
  • 相关阅读:
    Begin Example with Override Encoded SOAP XML Serialization
    State Machine Terminology
    How to: Specify an Alternate Element Name for an XML Stream
    How to: Publish Metadata for a WCF Service.(What is the Metadata Exchange Endpoint purpose.)
    Beginning Guide With Controlling XML Serialization Using Attributes(XmlSerializaiton of Array)
    Workflow 4.0 Hosting Extensions
    What can we do in the CacheMetaData Method of Activity
    How and Why to use the System.servicemodel.MessageParameterAttribute in WCF
    How to: Begin Sample with Serialization and Deserialization an Object
    A Test WCF Service without anything of config.
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2671366.html
Copyright © 2011-2022 走看看