C#程序实现动态调用DLL的研究.doc_第1页
C#程序实现动态调用DLL的研究.doc_第2页
C#程序实现动态调用DLL的研究.doc_第3页
C#程序实现动态调用DLL的研究.doc_第4页
C#程序实现动态调用DLL的研究.doc_第5页
已阅读5页,还剩22页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

c#程序实现动态调用dll的研究摘要:在csdn开发高手2004年第03期中的化功大法将dll嵌入exe一文,介绍了如何把一个动态链接库作为一个资源嵌入到可执行文件,在可执行文件运行时,自动从资源中释放出来,通过静态加载延迟实现函数的动态加载,程序退出后实现临时文件的自动删除,从而为解决“dll hell”提供了一种解决方案。这是一个很好的设计思想,而且该作者也用c+实现了,在internet上也有相似的vb程序,但在某一技术论坛上提起这种设计方法时,有网友提出:“这种方法好是好,但就是启动速度太慢”。这是因为程序启动时实现dll释放,然后再加载释放出来的dll,这个过程会耗费一定的时间。鉴于此问题,经过思索,提出另一个设计方案:dll作为资源文件嵌入程序,但不需进行dll释放及其重新加载。本文就是对该设计方案的原理分析及使用c#编程来实现该设计方案。关键词:动态调用dll,嵌入dll,c#正文:一、 dll与应用程序动态链接库(也称为dll,即为“dynamic link library”的缩写)是microsoft windows最重要的组成要素之一,打开windows系统文件夹,你会发现文件夹中有很多dll文件,windows就是将一些主要的系统功能以dll模块的形式实现。 动态链接库是不能直接执行的,也不能接收消息,它只是一个独立的文件,其中包含能被程序或其它dll调用来完成一定操作的函数(方法。注:c#中一般称为“方法”),但这些函数不是执行程序本身的一部分,而是根据进程的需要按需载入,此时才能发挥作用。dll只有在应用程序需要时才被系统加载到进程的虚拟空间中,成为调用进程的一部分,此时该dll也只能被该进程的线程访问,它的句柄可以被调用进程所使用,而调用进程的句柄也可以被该dll所使用。在内存中,一个dll只有一个实例,且它的编制与具体的编程语言和编译器都没有关系,所以可以通过dll来实现混合语言编程。dll函数中的代码所创建的任何对象(包括变量)都归调用它的线程或进程所有。下面列出了当程序使用 dll 时提供的一些优点:11) 使用较少的资源当多个程序使用同一个函数库时,dll 可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其他在 windows 操作系统上运行的程序。 2) 推广模块式体系结构dll 有助于促进模块式程序的开发。这可以帮助您开发要求提供多个语言版本的大型程序或要求具有模块式体系结构的程序。模块式程序的一个示例是具有多个可以在运行时动态加载的模块的计帐程序。 3) 简化部署和安装当 dll 中的函数需要更新或修复时,部署和安装 dll 不要求重新建立程序与该 dll 的链接。此外,如果多个程序使用同一个 dll,那么多个程序都将从该更新或修复中获益。当您使用定期更新或修复的第三方 dll 时,此问题可能会更频繁地出现。二、 dll的调用每种编程语言调用dll的方法都不尽相同,在此只对用c#调用dll的方法进行介绍。首先,您需要了解什么是托管,什么是非托管。一般可以认为:非托管代码主要是基于win 32平台开发的dll,activex的组件,托管代码是基于.net平台开发的。如果您想深入了解托管与非托管的关系与区别,及它们的运行机制,请您自行查找资料,本文件在此不作讨论。(一) 调用dll中的非托管函数一般方法首先,应该在c#语言源程序中声明外部方法,其基本形式是:dllimport(“dll文件”)修饰符 extern 返回变量类型 方法名称(参数列表)其中:dll文件:包含定义外部方法的库文件。修饰符:访问修饰符,除了abstract以外在声明方法时可以使用的修饰符。返回变量类型:在dll文件中你需调用方法的返回变量类型。方法名称:在dll文件中你需调用方法的名称。参数列表:在dll文件中你需调用方法的列表。注意:需要在程序声明中使用system.runtime.interopservices命名空间。 dllimport只能放置在方法声明上。dll文件必须位于程序当前目录或系统定义的查询路径中(即:系统环境变量中path所设置的路径)。返回变量类型、方法名称、参数列表一定要与dll文件中的定义相一致。若要使用其它函数名,可以使用entrypoint属性设置,如:dllimport(user32.dll, entrypoint=messageboxa)static extern int msgbox(int hwnd, string msg, string caption, int type);其它可选的 dllimportattribute 属性:charset 指示用在入口点中的字符集,如:charset=charset.ansi;setlasterror 指示方法是否保留 win32上一错误,如:setlasterror=true;exactspelling 指示 entrypoint 是否必须与指示的入口点的拼写完全匹配,如:exactspelling=false;preservesig指示方法的签名应当被保留还是被转换, 如:preservesig=true;callingconvention指示入口点的调用约定, 如:callingconvention=callingconvention.winapi;此外,关于“数据封送处理”及“封送数字和逻辑标量”请参阅其它一些文章2。c#例子:1. 启动vs.net,新建一个项目,项目名称为“tzb”,模板为“windows 应用程序”。2. 在“工具箱”的“ windows 窗体”项中双击“button”项,向“form1”窗体中添加一个按钮。3. 改变按钮的属性:name为 “b1”,text为 “用dllimport调用dll弹出提示框”,并将按钮b1调整到适当大小,移到适当位置。4. 在类视图中双击“form1”,打开“form1cs”代码视图,在“namespace tzb”上面输入“using system.runtime.interopservices;”,以导入该命名空间。5. 在“form1cs设计”视图中双击按钮b1,在“b1_click”方法上面使用关键字 static 和 extern 声明方法“msgbox”,将 dllimport 属性附加到该方法,这里我们要使用的是“user32dll”中的“messageboxa”函数,具体代码如下:dllimport(user32.dll, entrypoint=messageboxa) static extern int msgbox(int hwnd, string msg, string caption, int type); 然后在“b1_click”方法体内添加如下代码,以调用方法“msgbox”:msgbox(0, 这就是用 dllimport 调用 dll 弹出的提示框哦! , 挑战杯 ,0x30); 6. 按“f5”运行该程序,并点击按钮b1,便弹出如下提示框:(二) 动态装载、调用dll中的非托管函数在上面已经说明了如何用dllimport调用dll中的非托管函数,但是这个是全局的函数,假若dll中的非托管函数有一个静态变量s,每次调用这个函数的时候,静态变量s就自动加1。结果,当需要重新计数时,就不能得出想要的结果。下面将用例子说明:1. dll的创建1) 启动visual c+ 6.0;2) 新建一个“win32 dynamic-link library”工程,工程名称为“count”;3) 在“dll kind”选择界面中选择“a simple dll project”;4) 打开count.cpp,添加如下代码:/ 导出函数,使用“ _stdcall ” 标准调用 extern c _declspec(dllexport)int _stdcall count(int init); int _stdcall count(int init) /count 函数,使用参数 init 初始化静态的整形变量 s ,并使 s 自加 1 后返回该值 static int s=init; s+; return s; 5) 按“f7”进行编译,得到count.dll(在工程目录下的debug文件夹中)。2. 用dllimport调用dll中的count函数1) 打开项目“tzb”,向“form1”窗体中添加一个按钮。2) 改变按钮的属性:name为 “b2”,text为 “用dllimport调用dll中count函数”,并将按钮b1调整到适当大小,移到适当位置。3) 打开“form1cs”代码视图,使用关键字 static 和 extern 声明方法“count”,并使其具有来自 count.dll 的导出函数count的实现,代码如下:dllimport(count.dll) static extern int count(int init); 4) 在“form1cs设计”视图中双击按钮b2,在“b2_click”方法体内添加如下代码:messagebox.show( 用 dllimport 调用 dll 中的 count 函数, n 传入的实参为 0 ,得到的结果是: +count(0).tostring(), 挑战杯 ); messagebox.show( 用 dllimport 调用 dll 中的 count 函数, n 传入的实参为 10 ,得到的结果是: +count(10).tostring()+n 结果可不是想要的 11 哦! , 挑战杯 ); messagebox.show( 所得结果表明: n 用 dllimport 调用 dll 中的非托管 n 函数是全局的、静态的函数! , 挑战杯 ); 5) 把count.dll复制到项目“tzb”的bindebug文件夹中,按“f5”运行该程序,并点击按钮b2,便弹出如下三个提示框:第1个提示框显示的是调用“count(0)”的结果,第2个提示框显示的是调用“count(10)”的结果,由所得结果可以证明“用dllimport调用dll中的非托管函数是全局的、静态的函数”。所以,有时候并不能达到我们目的,因此我们需要使用下面所介绍的方法:c#动态调用dll中的函数。 3. c#动态调用dll中的函数因为c#中使用dllimport是不能像动态load/unload assembly那样,所以只能借助api函数了。在kernel32.dll中,与动态库调用有关的函数包括3:loadlibrary(或mfc 的afxloadlibrary),装载动态库。 getprocaddress,获取要引入的函数,将符号名或标识号转换为dll内部地址。freelibrary(或mfc的afxfreelibrary),释放动态链接库。它们的原型分别是:hmodule loadlibrary(lpctstr lpfilename);farproc getprocaddress(hmodule hmodule, lpcwstr lpprocname);bool freelibrary(hmodule hmodule);现在,我们可以用intptr hmodule=loadlibrary(“count.dll”);来获得dll的句柄,用intptr farproc=getprocaddress(hmodule,”_count4”);来获得函数的入口地址。但是,知道函数的入口地址后,怎样调用这个函数呢?因为在c#中是没有函数指针的,没有像c+那样的函数指针调用方式来调用函数,所以我们得借助其它方法。经过研究,发现我们可以通过结合使用system.reflection.emit及system.reflection.assembly里的类和函数达到我们的目的。为了以后使用方便及实现代码的复用,我们可以编写一个类。1) dld类的编写:1. 打开项目“tzb”,打开类视图,右击“tzb”,选择“添加”-“类”,类名设置为“dld”,即dynamic loading dll 的每个单词的开头字母。2. 添加所需的命名空间及声明参数传递方式枚举:using system.runtime.interopservices; / 用 dllimport 需用此 命名空间 using system.reflection; / 使用 assembly 类需用此 命名空间 using system.reflection.emit; / 使用 ilgenerator 需用此 命名空间 在“public class dld”上面添加如下代码声明参数传递方式枚举:/ / 参数传递方式枚举 ,byvalue 表示值传递 ,byref 表示址传递 / public enum modepass byvalue = 0x0001, byref = 0x0002 3. 声明loadlibrary、getprocaddress、freelibrary及私有变量hmodule和farproc:/ / 原型是 :hmodule loadlibrary(lpctstr lpfilename); / / dll 文件名 / 函数库模块的句柄 dllimport(kernel32.dll) static extern intptr loadlibrary(string lpfilename); / / 原型是 : farproc getprocaddress(hmodule hmodule, lpcwstr lpprocname); / / 包含需调用函数的函数库模块的句柄 / 调用函数的名称 / 函数指针 dllimport(kernel32.dll) static extern intptr getprocaddress(intptr hmodule, string lpprocname); / / 原型是 : bool freelibrary(hmodule hmodule); / / 需释放的函数库模块的句柄 / 是否已释放指定的 dll dllimport(kernel32,entrypoint=freelibrary,setlasterror=true) static extern bool freelibrary(intptr hmodule); / / loadlibrary 返回的函数库模块的句柄 / private intptr hmodule=intptr.zero; / / getprocaddress 返回的函数指针 / private intptr farproc=intptr.zero; 4. 添加loaddll方法,并为了调用时方便,重载了这个方法:/ / 装载 dll / / dll 文件名 public void loaddll(string lpfilename) hmodule=loadlibrary(lpfilename); if(hmodule=intptr.zero) throw(new exception( 没有找到 :+lpfilename+. ); 若已有已装载dll的句柄,可以使用loaddll方法的第二个版本:public void loaddll(intptr hmodule) if(hmodule=intptr.zero) throw(new exception( 所传入的函数库模块的句柄 hmodule 为空 . ); hmodule=hmodule; 5. 添加loadfun方法,并为了调用时方便,也重载了这个方法,方法的具体代码及注释如下:/ / 获得函数指针 / / 调用函数的名称 public void loadfun(string lpprocname) / 若函数库模块的句柄为空,则抛出异常 if(hmodule=intptr.zero) throw(new exception( 函数库模块的句柄为空 , 请确保已进行 loaddll 操作 !); / 取得函数指针 farproc = getprocaddress(hmodule,lpprocname); / 若函数指针,则抛出异常 if(farproc=intptr.zero) throw(new exception( 没有找到 :+lpprocname+ 这个函数的入口点 ); / / 获得函数指针 / / 包含需调用函数的 dll 文件名 / 调用函数的名称 public void loadfun(string lpfilename,string lpprocname) / 取得函数库模块的句柄 hmodule=loadlibrary(lpfilename); / 若函数库模块的句柄为空,则抛出异常 if(hmodule=intptr.zero) throw(new exception( 没有找到 :+lpfilename+. ); / 取得函数指针 farproc = getprocaddress(hmodule,lpprocname); / 若函数指针,则抛出异常 if(farproc=intptr.zero) throw(new exception( 没有找到 :+lpprocname+ 这个函数的入口点 ); 6. 添加unloaddll及invoke方法,invoke方法也进行了重载:/ / 卸载 dll / public void unloaddll() freelibrary(hmodule); hmodule=intptr.zero; farproc=intptr.zero; invoke方法的第一个版本:/ / 调用所设定的函数 / / 实参 / 实参类型 / 实参传送方式 / 返回类型 / 返回所调用函数的 object public object invoke(object objarray_parameter,type typearray_parametertype,modepass modepassarray_parameter,type type_return) / 下面 3 个 if 是进行安全检查 , 若不能通过 , 则抛出异常 if(hmodule=intptr.zero) throw(new exception( 函数库模块的句柄为空 , 请确保已进行 loaddll 操作 !); if(farproc=intptr.zero) throw(new exception( 函数指针为空 , 请确保已进行 loadfun 操作 ! ) ); if(objarray_parameter.length!=modepassarray_parameter.length) throw(new exception( 参数个数及其传递方式的个数不匹配 . ) ); / 下面是创建 myassemblyname 对象并设置其 name 属性 assemblyname myassemblyname = new assemblyname(); myassemblyname.name = invokefun; / 生成单模块配件 assemblybuilder myassemblybuilder =appdomain.currentdomain.definedynamicassembly(myassemblyname,assemblybuilderaccess.run); modulebuilder mymodulebuilder =myassemblybuilder.definedynamicmodule(invokedll); / 定义要调用的方法 , 方法名为“ myfun ”,返回类型是“ type_return ”参数类型是“ typearray_parametertype ” methodbuilder mymethodbuilder =mymodulebuilder.defineglobalmethod(myfun,methodattributes.public| methodattributes.static,type_return,typearray_parametertype); / 获取一个 ilgenerator ,用于发送所需的 il ilgenerator il = mymethodbuilder.getilgenerator(); int i; for (i = 0; i objarray_parameter.length; i+) / 用循环将参数依次压入堆栈 switch (modepassarray_parameteri) case modepass.byvalue: il.emit(opcodes.ldarg, i); break; case modepass.byref: il.emit(opcodes.ldarga, i); break; default: throw(new exception( 第 +(i+1).tostring() + 个参数没有给定正确的传递方式 . ) ); if (intptr.size = 4) / 判断处理器类型 il.emit(opcodes.ldc_i4, farproc.toint32(); else if (intptr.size = 8) il.emit(opcodes.ldc_i8, farproc.toint64(); else throw new platformnotsupportedexception(); il.emitcalli(opcodes.calli,callingconvention.stdcall,type_return,typearray_parametertype); il.emit(opcodes.ret); / 返回值 mymodulebuilder.createglobalfunctions(); / 取得方法信息 methodinfo mymethodinfo = mymodulebuilder.getmethod(myfun); return mymethodinfo.invoke(null, objarray_parameter);/ 调用方法,并返回其值 invoke方法的第二个版本,它是调用了第一个版本的:/ / 调用所设定的函数 / / 函数指针 / 实参 / 实参类型 / 实参传送方式 / 返回类型 / 返回所调用函数的 object public object invoke(intptr intptr_function,object objarray_parameter,type typearray_parametertype,modepass modepassarray_parameter,type type_return) / 下面 2 个 if 是进行安全检查 , 若不能通过 , 则抛出异常 if(hmodule=intptr.zero) throw(new exception( 函数库模块的句柄为空 , 请确保已进行 loaddll 操作 !); if(intptr_function=intptr.zero) throw(new exception( 函数指针 intptr_function 为空 ! ) ); farproc=intptr_function; return invoke(objarray_parameter,typearray_parametertype,modepassarray_parameter,type_return); 2) dld类的使用:1打开项目“tzb”,向“form1”窗体中添加三个按钮。name 和text属性分别为 “b3”、“用loadlibrary方法装载count.dll”,“b4”、“调用count方法”,“b5”、“卸载count.dll”,并调整到适当的大小及位置。2在“form1cs设计”视图中双击按钮b3,在“b3_click”方法体上面添加代码,创建一个dld类实例:/ / 创建一个 dld 类对象 / private dld myfun=new dld(); 3在“b3_click”方法体内添加如下代码:myfun.loaddll(count.dll); / 加载 count.dll myfun.loadfun(_count4); / 调入函数 count, _count4 是它的入口,可通过 depends 查看 4“form1cs设计”视图中双击按钮b4,在“b4_click”方法体内添加如下代码:object parameters = new object(int)0; / 实参为 0 type parametertypes = new typetypeof(int); / 实参类型为 int modepass themode=new modepassmodepass.byvalue; / 传送方式为值传 type type_return = typeof(int); / 返回类型为 int / 弹出提示框,显示调用 myfun.invoke 方法的结果,即调用 count 函数 messagebox.show( 这是您装载该 dll 后第 +myfun.invoke(parameters,parametertypes,themode,type_return).tostring() + 次点击此按钮。 , 挑战杯 ); 5“form1cs设计”视图中双击按钮b5,在“b5_click”方法体内添加如下代码:myfun.unloaddll(); 6按“f5”运行该程序,并先点击按钮b3以加载“count.dll”,接着点击按钮b4三次以调用3次“count(0)”,先后弹出的提示框如下: 这三个提示框所得出的结果说明了静态变量s 经初始化后,再传入实参“0”也不会改变其值为“0”。7点击按钮b5以卸载“count.dll”,再点击按钮b3进行装载“count.dll”,再点击按钮b4查看调用了“count(0)”的结果:从弹出的提示框所显示的结果可以看到又开始重新计数了,也就是实现了dll的动态装载与卸载了。(三) 调用托管dll一般方法c# 调用托管dll是很简单的,只要在“解决方案资源管理器”中的需要调用dll的项目下用鼠标右击“引用”,并选择“添加引用”,然后选择已列出的dll或通过浏览来选择dll文件,最后需要用using 导入相关的命名空间。(四) 动态调用托管dllc# 动态调用托管dll也需要借助system.reflection.assembly里的类和方法,主要使用了assembly.loadfrom。现在,用例子说明: 首先,启动vs.net,新建一个visual c# 项目,使用的模板为“类库”,名称为“cscount”,并在类“class1”中添加静态整型变量s及方法count:/ 由于 static 不能修饰方法体内的变量,所以需放在这里,且初始化值为 int.minvalue static int s=int.minvalue; public int count(int init) / 判断 s 是否等于 int.minvalue ,是的话把 init 赋值给 s if(s=int.minvalue) s=init; s+; /s 自增 1 return s; / 返回 s 然后,打开项目“tzb”,向“form1”窗体中添加一个按钮,name属性为“b6”,text属性为“用assembly类来动态调用托管dll”,调整到适当大小和位置,双击按钮b6,转入代码视图,先导入命名空间:using system.reflection; 接着添加invoke方法和b6_click方法代码:private object invoke(string lpfilename,string namespace,string classname,string lpprocname,object objarray_parameter) try / 载入程序集 assembly myassembly=assembly.loadfrom(lpfilename); type type=myassembly.gettypes(); foreach(type t in type) / 查找要调用的命名空间及类 if(t.namespace=namespace&t.name=classname) / 查找要调用的方法并进行调用 methodinfo m=t.getmethod(lpprocname); if(m!=null) object o=activator.createinstance(t); return m.invoke(o,objarray_parameter); else messagebox.show( 装载出错 !); /try catch(system.nullreferenceexception e) messagebox.show(e.message); /catch return (object)0; / invoke “b6_click”方法体内代码如下:/ 显示 count(0) 返回的值 messagebox.show( 这是您第 +invoke(cscount.dll,cscount,class1,count,new object(int)0).tostring()+ 次点击此按钮。 , 挑战杯 ); 最后,把项目“cscount”的bindebug文件夹中的cscount.dll复制到项目“tzb”的bindebug文件夹中,按“f5”运行该程序,并点击按钮b6三次,将会弹出3个提示框,内容分别是“这是您第 1次点击此按钮。”、“这是您第 2次点击此按钮。”、“这是您第 3次点击此按钮。”,由此知道了静态变量s在这里的作用。(五) c#程序嵌入dll的调用 dll文件作为资源嵌入在c#程序中,我们只要读取该资源文件并以“byte”返回,然后就用“assembly load(byte);”得到dll中的程序集,最后就可以像上面的invoke方法那样对dll中的方法进行调用。当然不用上面方法也可以,如用接口实现动态调用,但dll中必须有该接口的定义并且程序中也要有该接口的定义;也可用反射发送实现动态调用4。现在我只对像上面的invoke方法那样对dll中的方法进行调用进行讨论,为了以后使用方便及实现代码的复用,我们可以结合上一个编写一个类。1) ldfs类的编写:在项目“tzb”中新建一个名为ldfs的类,意为“load dll from resource”,请注意,在这个类中“resource”不只是嵌入在exe程序中的资源,它也可以是硬盘上任意一个dll文件,这是因为ldfs的类中的方法loaddll有些特别,就是先从程序的内嵌的资源中查找需加载的dll,如果找不到,就查找硬盘上的。首先导入所需的命名空间:using system.io; / 对文件的读写需要用到此命名空间 using system.reflection; / 使用 assembly 类需用此命名空间 using system.reflection.emit; / 使用 ilgenerator 需用此命名空间 声明一静态变量myassembly:/ 记录要导入的程序集 static assembly myassembly; 添加loaddll方法:private byte loaddll(string lpfilename) assembly nowassembly = assembly.getentryassembly(); stream fs=null; try / 尝试读取资源中的 dll fs = nowassembly.getmanifestresourcestream(nowassembly.getname().name+.+lpfilename); finally / 如果资源没有所需的 dll ,就查看硬盘上有没有,有的话就读取 if (fs=null&!file.exists(lpfilename) throw(new exception( 找不到文件 :+lpfilename); else if(fs=null&file.exists(lpfilename) filestream fs = new filestream(lpfilename, filemode.open); fs=(stream)fs; byte buffer = new byte(int) fs.length; fs.read(buffer, 0, buffer.length); fs.close(); return buffer; / 以 byte 返回读到的 dll 添加unloaddll方法来卸载dll:public void unloaddll() / 使 myassembly 指空 myassembly=null; 添加invoke方法来进行对dll中方法的调用,其原理大体上和“form1cs”中的方法invoke相同,不过这里用的是“assembly.load”,而且用了静态变量myassembly来保存已加载的dll,如果已加载的话就不再加载,如果还没加载或者已加载的不同现在要加载的dll就进行加载,其代码如下所示:public object invoke(string lpfilename,string namespace,string classname,string lpprocname,object objarray_parameter) try / 判断 myassembly 是否为空或 myassembly 的命名空间不等于要调用方法的命名空间,如果条件为真,就用 assembly.load 加载所需 dll 作为程序集 if(myassembly=null|myassembly.getname()

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论