DLL地狱及其解决方案

别再掉进DLL地狱的陷阱里(DLL_Hell 别再掉进DLL地狱的陷阱里(DLL_Hell)~.NET解决之道 立即下载

DLL地狱及其解决方案

原作者:Ivan S Zapreev

译者:陆其明
概要

本文将要介绍DLL的向后兼容性问题,也就是著名的“DLL Hell”问题。首先我会列出自己的研究结果,其中包括其它一些研究者的成果。在本文的最后,我还将给出“DLL Hell”问题的一个解决方案。


介绍

我曾经接受过一个任务,去解决一个DLL版本更新的问题————某个公司给用户提供了一套SDK,这个SDK是由一系列DLL组成的;DLL中导出了很多类,用户使用这些类(直接使用或派生新的子类)来继续他们的C++程序开发。用户在使用这些DLL时没有得到很详细的使用说明(比如使用这些DLL中导出的类有什么限制等)。当这些DLL更新为新的版本之后,他们发现他们开发的基于这些DLL的应用程序会经常崩溃(他们的应用程序从SDK的导出类派生了新的子类)。为了解决这个问题,用户必须重新编译他们的应用程序,重新连接新版本的SDK DLL。

我将对这个问题给出我的研究结果,同时还有我从其它地方搜集过来的相关信息。最后,我将来解决这个“DLL Hell”问题。


研究结果

就我个人的理解,这个问题是由SDK DLL中导出的基类改动之后引起的。我查看了一些文章后发现,DLL的向后兼容性问题其实早有人提出。但作为一个实在的研究者,我决定自己做一些试验。结果,我发现如下的问题:

1. 在DLL的导出类中增加一个新的虚函数将导致如下问题:
(1)如果这个类以前就有一个虚函数B,此时在它之前增加一个新的虚函数A。这样,我们改变了类的虚函数表。于是,表中的第一个函数指向了函数A(而不是原来的B)。此时,客户程序(假设没有在拿到新版本的DLL之后重新编译、连接)调用函数B就会产生异常。因为此时调用函数B实际上转向了调用函数A,而如果函数A和函数B的参数类型、返回值类型迥异的话问题就出来了!
(2)如果这个类原本没有虚函数(它的父类也没有虚函数),那么给这个类增加一个新的虚函数(或者在它的父类增加一个虚函数)将导致新增加一个类成员,这个成员是一个指针类型的,指向虚函数表。于是,这个类的尺寸将会被改变(因为增加了一个成员变量)。这种情况下,客户程序如果创建了这个类的实例,并且需要直接或间接修改类成员的值的时候就会有问题了。因为虚函数表的指针是作为类的第一个成员加入的,也就是说,原本这个类定义的成员因为虚函数表指针的加入而都产生了地址的偏移。客户程序对原成员的操作自然就出现异常了。
(3)如果这个类原本就有虚函数(或者只要它的父类有虚函数),而且这个类被导出了,被客户程序当作父类来用。那么,我们不要给这个类增加虚函数!不仅在类声明的开头不能加,即使在末尾处也不能加。因为加入虚函数会导致虚函数表内的函数映射产生偏移;即使你将虚函数加在类声明的末尾,这个类的派生类的虚函数表也会因此产生偏移。

2. 在DLL的导出类中增加一个新的成员变量将导致如下问题:
   (1)给一个类增加一个成员变量将导致类尺寸的改变(给原本有虚函数表的类增加一个虚函数将不会改变类的尺寸)。假设这个成员增加在类声明的最后。如果客户程序为创建这个类的实例少分配了内存,那么可能在访问这个成员时导致内存越界。
   (2)如果在原有的类成员中间增加一个新的成员,情况会更糟糕。因为这样会导致原有类成员的地址产生偏移。客户程序操作的是一个错误的地址表,对于新成员后面的成员尤其是这样(它们都因为新成员的加入而导致了自己在类中的偏移的变化)。

(注:上述的客户程序就是指使用SDK DLL的应用程序。)

除了上面这些原因外,还有其它操作会导致DLL的向后兼容性问题。下面列出了解决(大部分)这些问题的方法。


DLL编码约定简述

下面是我搜集到的所有的解决方案,其中一些是从网上的文章中拿来的,一些是跟不同的开发者交流后得到的。

下面的约定主要针对DLL开发,而且是为解决DLL的向后兼容性问题:

1. 编码约定:
   (1)DLL的每个导出类(或者它的父类)至少包含一个虚函数。这样,这个类就会始终保存一个指向虚函数表的指针成员。这么做可以方便后来新的虚函数的加入。
   (2)如果你要给一个类增加一个虚函数,那么将它加在所有其它虚函数的后面。这样就不会改变虚函数表中原有函数的地址映射顺序。
   (3)如果你打算以后给一个类扩充类成员,那么现在预留一个指向一个数据结构的指针。这样的话,增加一个成员直接在这个数据结构中修改,而不是在类中修改。于是,新成员的加入不会导致类尺寸的改变。当然,为了访问新成员,需要给这个类定义几个操作函数。这种情况下,DLL必须是被客户程序隐式(implicitly)连接的。
   (4)为了解决前一点的问题,也可以给所有的导出类设计一个纯接口的类,但此时,客户程序将无法从这些导出类继续派生,DLL导出类的层次机构也将无法维持。
   (5)发布两个版本的DLL和LIB文件(Debug版本和Release版本)。因为如果只发布Release版本,开发者将无法调试他们的程序,因为Release版与Debug版使用了不同的堆(Heap)管理器,因而当Debug版本的客户程序释放Release版本DLL申请的内存时,会导致运行时错误(Runtime failure)。有一种办法可以解决这个问题,就是DLL同时提供申请和释放内存的函数供客户程序调用;DLL中也保证不释放客户程序申请的内容。通常遵守这个约定不是那么简单!
   (6)在编译的时候,不要改变DLL导出类函数的默认参数,如果这些参数将被传递到客户程序的话。
   (7)注意内联(inline)函数的更改。
   (8)检查所有的枚举没有默认的元素值。因为当增加/删除一个新的枚举成员,你可能移动旧枚举成员的值。这就是为什么每一个成员应该拥有一个唯一标识值。如果枚举可以被扩展,也应该对其进行文档说明。这样,客户程序开发者就会引起注意。
   (9)不要改变DLL提供的头文件中定义的宏。

2. 对DLL进行版本控制:如果主要的DLL发生了改变,最好同时将DLL文件的名字也改掉,就象微软的MFC DLL一样。例如,DLL文件可以按照如下格式命名:Dll_name_xx.dll,其中xx就是DLL的版本号。有时候DLL中做了很大的改动,使得向后兼容性问题无法解决。此时应该生成一个全新的DLL。将这个新DLL安装到系统时,旧的DLL仍然保留。于是,旧的客户程序仍然能够使用旧的DLL,而新的客户程序(使用新DLL编译、连接)可以使用新的DLL,两者互不干涉。

3. DLL的向后兼容性测试:还有很多很多中可能会破坏DLL的向后兼容性,因此实施DLL的向后兼容性测试是非常必要的!

接下去,我将来讨论一个虚函数的问题,以及对应的一个解决方案。


虚函数与继承

首先来看一下如下的虚函数和继承结构:

/**********DLL导出的类 **********/
class EXPORT_DLL_PREFIX VirtFunctClass{
public:
    VirtFunctClass(){}
    ~VirtFunctClass(){}
    virtual void DoSmth(){
        //this->DoAnything();
              // Uncomment of this line after the corresponding method
              //will be added to the class declaration
    }
    //virtual void DoAnything(){}
         // Adding of this virtual method will make shift in
         // table of virtual methods
};

/**********客户程序,从DLL导出类派生一个新的子类**********/
class VirtFunctClassChild : public VirtFunctClass {
public:
    VirtFunctClassChild() : VirtFunctClass (){}
    ~VirtFunctClassChild(){};
    virtual void DoSomething(){}
};

假设上面的两个类,VirtFunctClass在my.dll中实现,而VirtFunctClassChild在客户程序中实现。接下去,我们做一些改变,将如下两个注释行放开:
//virtual void DoAnything(){}
和
//this->DoAnything();

也就是说,DLL导出的类作了改动!现在如果客户程序没有重新编译,那么客户程序中的VirtFunctClassChild将不知道DLL中VirtFunctClass类已经改变了:增加了一个虚函数void DoAnything()。因此,VirtFunctClassChild类的虚函数表仍然包含两个函数的映射:
1. void DoSmth()
2. void DoSomething()

而事实上这已经不对了,正确的虚函数表应该是:
1. void DoSmth()
2. void DoAnything()
3. void DoSomething()

问题就在于,当实例化VirtFunctClassChild之后,如果调用它的void DoSmth()函数,DoSmth()函数转而要调用void DoAnything()函数,但此时基类VirtFunctClass只知道要调用虚函数表中的第二个函数,而VirtFunctClassChild类的虚函数表中的第二个函数仍然是void DoSomething(),于是问题就出来了!

另外,禁止在DLL的导出类的派生类(上例中的VirtFunctClassChild)中增加虚函数也是于事无补的。因为,如果VirtFunctClassChild类中没有virtual void DoSomething()函数,基类中的void DoAnything()函数(虚函数表中的第二个函数)调用将会指向一个空的内存地址(因为VirtFunctClassChild类维持的虚函数表仅仅维持有一个函数地址)。

现在可以看出,在DLL的导出类中增加虚函数是一个多么严重的问题!不过,如果虚函数是用来处理回调事件的,我们有办法来解决这个问题(下文有介绍)。

COM及其它

现在可以看出,DLL的向后兼容性问题是一个很出名的问题。解决这些问题,不仅可以借助于一些约定,而且可以通过其它一些先进的技术,比如COM技术。因此,如果你想摆脱“DLL Hell”问题,请使用COM技术或者其它一些合适的技术。

让我们回到我接受的那个任务(我在本文开头的地方讲到的那个任务)————解决一个使用DLL的产品的向后兼容性问题。

我对COM有些了解,因此我的第一个建议是使用COM技术来克服那个项目中的所有问题。但这个建议因为如下原因最终被否决了:
1. 那个产品已经在某个内部层中有一个COM服务器。
2. 将一大堆接口类重写到COM的形式,投入比较大。
3. 因为那个产品是DLL库,而且已经有很多应用程序在使用它了。因此,他们不想强制他们的客户重写他们的应用程序。

换句话说,我被要求完成的任务是,以最小的代价来解决这个DLL向后兼容性问题。当然,我应该指出,这个项目最主要的问题在于增加新的成员和接口类上的虚回调函数。第一个问题可以简单地通过在类声明中增加一个指向一个数据结构的指针来解决(这样可以任意增加新的成员)。这种方法我在上面已经提到过。但是第二个问题,虚回调函数的问题是新提出的。因此,我提出了下面的最小代价、最有效的解决方法。


虚回调函数与继承

然我们想象一下,我们有一个DLL,它导出了几个类;客户应用程序会从这些导出类派生新的类,以实现虚函数来处理回调事件。我们想在DLL中做一个很小的改动。这个改动允许我们将来可以给导出类“无痛地”增加新的虚回调函数。同时,我们也不想影响使用当前版本DLL的应用程序。我们期望的就是,这些应用程序只有在不得已的时候才协同新版本的DLL进行一次重新编译。因此,我给出了下面的解决方案:

我们可以保留DLL导出类中的每个虚回调函数。我们只需记住,在任何一个类定义中增加一个新的虚函数,如果应用程序不协同新版本的DLL重新编译,将导致严重的问题。我们所做的,就是想要避免这个问题。这里我们可以一个“监听”机制。如果在DLL导出类中定义并导出的虚函数被用作处理回调,我们可以将这些虚函数转移到独立的接口中去。

让我们来看下面的例子:

// 如果想要测试改动过的DLL,请将下面的定义放开
//#define DLL_EXAMPLE_MODIFIED

#ifdef DLL_EXPORT
    #define DLL_PREFIX __declspec(dllexport)
#else
    #define DLL_PREFIX __declspec(dllimport)
#endif

/********** DLL的导出类 **********/
#define CLASS_UIID_DEF static short GetClassUIID(){return 0;}
#define OBJECT_UIID_DEF virtual short
      GetObjectUIID(){return this->GetClassUIID();}

// 所有回调处理的基本接口
struct DLL_PREFIX ICallBack
{
    CLASS_UIID_DEF
    OBJECT_UIID_DEF
};

#undef CLASS_UIID_DEF

#define CLASS_UIID_DEF(X) public: static
     short GetClassUIID(){return X::GetClassUIID()+1;}

// 仅当DLL_EXAMPLE_MODIFIED宏已经定义的时候,进行接口扩展
#if defined(DLL_EXAMPLE_MODIFIED)
// 新增加的接口扩展
struct DLL_PREFIX ICallBack01 : public ICallBack
{
    CLASS_UIID_DEF(ICallBack)
    OBJECT_UIID_DEF
    virtual void DoCallBack01(int event) = 0; // 新的回调函数
};
#endif // defined(DLL_EXAMPLE_MODIFIED)

class DLL_PREFIX CExample{
public:
    CExample(){mpHandler = 0;}
    virtual ~CExample(){}
    virtual void DoCallBack(int event) = 0;
    ICallBack * SetCallBackHandler(ICallBack *handler);
    void Run();
private:
    ICallBack * mpHandler;
};

很显然,为了给扩展DLL的导出类(增加新的虚函数)提供方便,我们必须做如下工作:
1. 增加ICallBack * SetCallBackHandler(ICallBack *handler);函数;
2. 在每个导出类的定义中增加相应的指针;
3. 定义3个宏;
4. 定义一个通用的ICallBack接口。

为了演示给CExample类增加新的虚回调函数,我在这里增加了一个ICallBack01接口的定义。很显然,新的虚回调函数应该加在新的接口中。每次DLL更新都新增一个接口(当然,每次DLL更新时,我们也可以给一个类同时增加多个虚回调函数)。

注意,每个新接口必须从上一个版本的接口继承。在我的例子中,我只定义了一个扩展接口ICallBack01。如果DLL再下个版本还要增加新的虚回调函数,我们可以在定义一个ICallBack02接口,注意ICallBack02接口要从ICallBack01接口派生,就跟当初ICallBack01接口是从ICallBack接口派生的一样。

上面代码中还定义了几个宏,用于定义需要检查接口版本的函数。例如我们要为新接口ICallBack01增加新函数DoCallBack01,如果我们要调用ICallBack * mpHandler; 成员的话,就应该在CExample类进行一下检查。这个检查应该如下实现:

if(mpHandler != NULL && mpHandler->GetObjectUIID()>=ICallBack01::GetClassUIID()){
    ((ICallBack01 *) mpHandler)->DoCallBack01(2);
}

我们看到,新回调接口增加之后,在CExample类的实现中只需简单地插入新的回调调用。

现在你可以看出,我们上述对DLL的改动并不会影响客户应用程序。唯一需要做的,只是在采用这种新设计后的第一个DLL版本(为DLL导出类增加了宏定义、回调基本接口ICallBack、设置回调处理的SetCallBackHandler函数,以及ICallBack接口的指针)发布后,应用程序进行一次重编译。(以后扩展新的回调接口,应用程序的重新编译不是必需的!)

以后如果有人想要增加新的回调处理,他就可以通过增加新接口的方式来实现(向上例中我们增加ICallBack01一样)。显然,这种改动不会引起任何问题,因为虚函数的顺序并没有改变。因此应用程序仍然以以前的方式运行。唯一你要注意的是,除非你在应用程序中实现了新的接口,否则你就接收不到新增加的回调调用。

我们应该注意到,DLL的用户仍然能够很容易与它协同工作。下面是客户程序中的某个类的实现例子:

// 如果DLL_EXAMPLE_MODIFIED没有定义,使用以前版本的DLL
#if !defined(DLL_EXAMPLE_MODIFIED)
// 此时没有使用扩展接口ICallBack01
class CClient : public CExample{
public:
    CClient();
    void DoCallBack(int event);
};

#else // !defined(DLL_EXAMPLE_MODIFIED)
// 当DLL增加了新接口ICallBack01后,客户程序可以修改自己的类
// (但不是必须的,如果他不想处理新的回调事件的话)
class CClient : public CExample, public ICallBack01{
public:
    CClient();
    void DoCallBack(int event);

    // 声明DoCallBack01函数(客户程序要实现它,以处理新的回调事件)
    // (DoCallBack01是ICallBack01接口新增加的虚函数)
    void DoCallBack01(int event);
};
#endif // defined(DLL_EXAMPLE_MODIFIED)

Win11家庭版彻底关闭Hyper-V的3种方法(实测有效) 本文详细介绍了Win11家庭版彻底关闭Hyper-V的3种实测有效方法,解决与VMware的兼容性问题。包括命令行禁用虚拟化功能、微软官方工具DG_Readiness深度清理以及组策略与功能组件联用方案,帮助用户高效禁用Hyper-V并优化系统性能。 阅读详情

相关推荐

从零开始搭建GD32F103CBT6开发环境:VSCode配置与实战技巧

本文详细介绍了如何从零开始为国产MCU GD32F103CBT6搭建高效的VSCode开发环境。内容涵盖工具链安装、VSCode核心插件配置、工程目录结构规划、关键配置文件编写以及Makefile实战,旨在帮助开发者摆脱传统IDE束缚,利用现代化工具链提升嵌入式开发效率。

weixin_29158413的博客 448

DLL导出类避免地狱问题的完美解决方案

 DLL动态链接库是程序复用的重要方式,DLL可以导出函数,使函数被多个程序复用,DLL中的函数实现可以被修改而无需重新编译和连接使用该DLL的应用程序。作为一名面向对象的程序员,希望DLL可以导出类,以便在类的层次上实现复用。所幸的是,DLL确实也可以导出类。然而事实却没这么简单,导出类的DLL在维护和修改时有很多地方必需很小心,增加成员变量、修改导出类的基类等操作都可能导致意想不到

2809

RK3588与银河麒麟V10 SP1 2403深度适配:国产化硬件与操作系统的完美融合

本文深入探讨了RK3588处理器与银河麒麟V10 SP1 2403操作系统的深度适配,展示了国产化硬件与操作系统的完美融合。通过性能验证和实际应用测试,证明了这套组合在多媒体处理、AI推理和日常办公中的卓越表现,实现了完全自主可控的计算平台。

weixin_29194255的博客 249

DLL Hell的产生和如何有效的避免

Dll Hell究竟是何物 DLL HELL字面意思是DLL"地狱",是由于DLL组件升级引起的程序不能运行的情况。共享模块常常被编译为Dll文件。Dll共享的方式虽然很不错,但是它自身存在致命的缺陷。由于Dll模块可以被重用,这样多个程序可能使用一个Dll模块 ,如果这个Dll模块升级了,就很有可能出现其中某个程序无法和新的Dll模块很好的兼容起来,导致程序不能运行的情况,这种情况

flyound的专栏 2468

Cadence17.2 > OrCAD Capture CIS > 设计规则检查(Design Rule Check)DRC学习记录详解

选中设计完成的DSN文件,选择Tool->Design Rule Check,弹出下面对话框,详解如下。 一、Design Rule Check对话框选项详解 Design Rules Options tab Set the scope, mode and type of design rule (electrical and / or physical) test options for the design rules check. 为

DL_62532的专栏 1万+

GAMES104:02引擎架构分层-学习笔记

用户层工具层(Tool Layer):编辑器、工具界面功能层(Function Layer):渲染Rendering、动画Animation、物理Physics、相机Camera和实现游戏性的脚本Script、状态机FSM、AI等功能模块资源层(Resource Layer):各类图片、动画、音频资源加载核心层(Core Layer):核心功能如内存管理、容器分配、数据计算、多线程处理等平台层(Platform Layer):对接不同游戏平台、主机和硬件。

噗噗噗的博客 1267

Windows下动态链接之三:DLL Hell !

不止Linux下关于共享库存在版本兼容性困扰问题,Windows下DLL共享库的使用问题更甚。很多Windows的应用程序在发布release版本时会一次性将所有用到的DLL一起打包形成一个大的安装包,用户只需一键安装,无需关注具体的DLL文件的配置问题。但也正是这种黑盒操作导致了可能存在某次安装,将系统中的新版本DLL文件被旧版本DLL给覆盖掉,虽然安装的程序可以运行了,但是其他程序可能出现问题了

墨篙和小奶猫 1362

AI赋能软件测试:从冒烟测试到全流程智能化的效率革命

软件测试是确保软件质量的关键环节,其核心原理是通过预设用例验证系统功能是否符合预期。传统测试方法依赖人工编写和维护脚本,面临维护成本高、适应性差等挑战。随着人工智能技术的发展,特别是自然语言处理和计算机视觉的成熟,AI为测试领域带来了新的技术价值。通过让AI理解应用界面和业务逻辑,可以实现测试用例的智能生成、自主执行和结果分析,显著提升测试效率和覆盖率。在实际应用场景中,AI驱动的冒烟测试能够动态适应UI变化,自动分析失败日志,将测试人员从重复劳动中解放出来,专注于更复杂的质量策略设计。本文基于AI在冒烟测

weixin_28841265的博客 306

DLL“地狱”的原因及其解决方案

转自:http://www.yesky.com/80/1910580.shtml  概要   本文将要介绍DLL的向后兼容性问题,也就是著名的“DLL Hell”问题。首先我会列出自己的研究结果,其中包括其它一些研究者的成果。在本文的最后,我还将给出“DLL Hell”问题的一个解决方案。  介绍  我曾经接受过一个任务,去解决一个DLL版本更新的问题————某个公司给用户提供了一套S

Peter的专栏 1551

深度学习PyTorch代码模板——模型训练框架、数据处理、绘图…

个人用深度学习代码模板:模型训练框架、数据处理、绘图等。待补充。

https://www.hyperplasma.top 1905

YOLOv8优化:特征融合 | 金字塔稀疏 Transformer(PST)——一个轻量级、即插即用的模块 | 25年7月最新成果

将 PST 嵌入当前最先进的实时检测模型(YOLOv11-N/S/M)后,在 MS COCO 上的 mAP 分别提升 0.9%、0.5%、0.4%,而延迟增加极小。

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 611

经典编程:DLL地狱及其解决方案

本文将要介绍DLL的向后兼容性问题,也就是著名的“DLL Hell”问题。首先我会列出自己的研究结果,其中包括其它一些研究者的成果。在本文的最后,我还将给出“DLL Hell”问题的一个解决方案。    介绍    我曾经接受过一个任务,去解决一个DLL版本更新的问题————某个公司给用户提供了一套SDK,这个SDK是由一系列DLL组成的;DLL中导出了很多类,用户使用这些类(直接使用或派生新的子

冷剑魂的专栏——垃圾场 1382

点云数据处理:体素滤波算法

体素滤波是一种简单而有效的点云数据处理方法,可用于去除离群点和平滑点云表面。体素滤波是一种常用的点云数据处理算法,用于去除离群点并平滑点云表面。在本文中,我们将介绍CloudCompare和PCL(Point Cloud Library)中的体素滤波算法,并提供相应的源代码示例。PCL是一个专门用于点云处理的开源库,提供了一系列高效的点云处理算法和工具。然后,我们创建了一个。方法执行体素滤波,并将滤波后的点云保存为"filtered_cloud.ply"文件。方法来执行体素滤波,指定体素大小为0.01。

ZyqfCss的博客 388

[百度空间] [转]DLL地狱及其解决方案

DLL地狱及其解决方案 原作者:Ivan S Zapreev 译者:陆其明概要 本文将要介绍DLL的向后兼容性问题,也就是著名的“DLL Hell”问题。首先我会列出自己的研究结果,其中包括其它一些研究者的成果。在本文的最后,我还将给出“DLL Hell”问题的一个解决方案。 介绍 我曾经接受过一个任务,去解决一个DLL版本更新的问题————某个公司给用户提供了一套SDK,这个SDK是由...

weixin_30909575的博客 107

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Windows 动态链接库 DLL 浅析

一、概念 DLL:Dynamic Link Library,即动态链接库,这种库包含了可由多个程序同时使用的代码和数据。 它是microsoft在windows操作系统中实现共享函数库概念的一种实现方式。其中windows中 一些作为DLL实现的文件有: ActiveX控件(.ocx)文件:如windows上的日历控件。控制面板(.cpl)文件:控制面板中的每一项都是一个专用的DLL

heyabo 3万+

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

windows -COM技术+DLL

Office中的COM对象简介 开发者可以使用多种程序语言通过COM技术操作文档对象而无需了解文件格式,大大丰富了应用程序功能。 COM技术的局限性表现在较以来Windows操作系统,不支持其他类型操作系统平台。 COM程序开发遵照下面的5个步骤: 安装office软件产品 创建程序项目 项目添加引用Office产品的COM库 源代码中添加相应的命名空间 使用类方法操作Office对象 ...

柯同学要谦虚 1240

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

上一篇: 使用多个第三方类库的问题(DLL内存分配与共享)
下一篇: 一个真正的Singleton
wang37921
博客等级 码龄18年 14粉丝 31原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值