我正在一些图形API(DirectX9和DirectX11)上面写一个抽象层,我希望你的意见。
传统上我会为每个想抽象的概念创建一个基类
因此,在典型的OO方式中,我将有一个Shader类和2个子类DX9Shader和DX11Shader。
我会重复纹理等过程...当我需要实例化它们时,我有一个抽象工厂,它将根据当前的图形API返回相应的子类。
在RAII之后,返回的指针将封装在std :: shared_ptr。
到目前为止一直很好,但就我而言,这种方法存在一些问题:
这促使我重新设计我做事的方式: 我以为我可以只返回指向资源的原始指针并让图形API清理干净,但仍然存在客户端悬挂指针和接口问题的问题。 我甚至考虑像COM这样的手动引用计数,但我认为这将是一个倒退(如果我错了,请纠正我,来自shared_ptr世界,手动引用计数似乎是原始的)。
然后我看到了Humus的工作,其中所有的图形类都由整数ID表示(很像OpenGL的作用)。
创建一个新对象只返回其整数ID,并在内部存储指针;它完全不透明!
代表抽象的类(例如DX9Shader等......)都隐藏在设备API后面,这是唯一的接口。
如果想要设置纹理,只需要调用device-> SetTexture(ID),其余的就在幕后发生。
失败的原因是API的隐藏部分膨胀,需要大量的锅炉板代码才能使其工作,而且我不是一个全能类的粉丝。
任何想法/想法?
答案 0 :(得分:3)
重要吗?对于该对象的用户,它只是一个不透明的句柄。它的实际实现类型并不重要,只要我可以将句柄传递给你的API函数并让它们用对象做东西。
您可以轻松更改这些句柄的实现,因此,无论您现在现在,都可以轻松实现。
只需将句柄类型声明为指针或整数的typedef,并确保所有客户端代码都使用typedef名称,然后客户端代码不依赖于您选择代表句柄的特定类型。
现在就去寻找简单的解决方案,如果/当你遇到问题因为太简单时,请改变它。
答案 1 :(得分:3)
你说主要的问题是DLL在卸载时仍然有一个指向其内部的指针。嗯...... 不要那样做。您有一个类实例,该成员在该DLL中实现。从根本上说,只要存在这些类实例,就可以卸载该DLL的错误。
因此,您需要对如何使用此抽象负责。正如您需要对从DLL加载的任何代码负责一样:在卸载DLL之前,必须清除来自DLL的内容。你如何做到这一点取决于你。您可以为DLL返回的每个对象增加一个内部引用计数,并仅在所有引用的对象消失后卸载DLL。或者其他什么,真的。
毕竟,即使您使用这些不透明的数字或其他什么,如果在卸载DLL时调用该数字上的其中一个API函数会发生什么?哎呀...所以它并没有真正为你买任何保护。你必须对此负责。
您可能不会考虑的数字方法的缺点是:
知道某个对象实际上是的能力降低了。 API调用可能会失败,因为您传递的数字实际上不是对象。或者更糟糕的是,如果将着色器对象传递给采用纹理的函数会发生什么?也许我们正在讨论一个带着色器和纹理的函数,你不小心忘记了参数的顺序?如果那些代码是对象指针,C ++的规则将不允许代码甚至编译。但是整数?都很好;你只会遇到运行时错误。
性能。每个API调用都必须在哈希表或其他东西中查找这个数字,以获得一个可以使用的实际指针。如果它是一个哈希表(即:一个数组),那么它可能相当小。但它仍然是间接的。而且由于你的抽象似乎非常低级,因此在这个级别上的任何性能损失在性能关键的情况下都会受到严重损害。
缺乏RAII和其他范围机制。当然,您可以编写一个shared_ptr
- esque对象来创建和删除它们。但如果使用实际指针,则不必这样做。
这似乎不值得。
答案 2 :(得分:0)
关于你的p。 2:在库之前总是卸载客户端。
每个进程都有其库依赖关系树,其中.exe为树根,中间级为用户Dll,低级为系统库。进程从低级加载到高级,最后加载树根(exe)。从根开始卸载进程,最后卸载低级库。这样做是为了防止你正在谈论的情况。
当然,如果您手动加载/卸载库,则此顺序会更改,您有责任保持指针有效。