从外部代码加载阶段
Loading Stages from external code
我写了一个基于管道和过滤器的架构。为了避免混淆,过滤器在我的代码中被称为 "Stages"。这是基本思想:
我希望其他开发人员能够实现他们自己的阶段 class,然后我可以将其添加到 运行 时已经存在的阶段列表中。
我已经阅读了一段时间,它们似乎对动态代码加载有限制。我当前的舞台 class 看起来像这样:
class Stage
{
public:
void Process();
const uint16_t InputCount();
const uint16_t OutputCount();
void SetOutputPipe(size_t idx, Pipe<AmplitudeVal> *outputPipe);
void SetInputPipe(size_t idx, Pipe<AmplitudeVal> *inputPipe);
protected:
Stage(const uint16_t inputCount, const uint16_t outputCount);
virtual void init() {};
virtual bool work() = 0;
virtual void finish() {};
protected:
const uint16_t mInputCount;
const uint16_t mOutputCount;
std::vector< Pipe<AmplitudeVal>* > mInputs;
std::vector< Pipe<AmplitudeVal>* > mOutputs;
};
AmplitudeVal 只是浮点数的别名。此 class 仅包含对其连接的管道的引用(mInputs 和 mOutputs),它不处理任何算法 activity。我想尽可能少地公开,以便于外部开发人员使用。现在这个 class 只依赖于管道头和一些基本的配置文件。大多数处理加载 DLL 的示例都建议 class 只有纯虚函数,几乎没有任何成员变量。我不确定我应该做什么。
我知道你想在 DLL 中有一个阶段,并让用户用户在你的 DLL 上派生他们的工作。
场景 1:消费者和 DLL 使用相同的编译器和相同的标准库构建
如果消费者使用相同的编译器、兼容的编译选项,并且双方使用相同的共享标准库(默认使用 MSVC),那么您的解决方案 应该按原样工作
(参见 。)
场景 2:消费者和 DLL 使用相同的编译器但不同的库构建
如果一方使用不同的标准库或其链接选项(例如,如果您为 DLL 使用静态链接库,为消费者使用共享库),那么您必须确保所有对象始终 created/released 在同一侧(因为 DLL 和应用程序将各自使用自己的分配函数和不同的内存池)。
这将非常困难,因为:
- 数据的继承
- 虚析构函数
- 标准容器的存储管理(在 DLL 和消费者中会有所不同,尽管源代码给人的印象可能是相同的)
朝着正确方向迈出的第一步是 将所有数据隔离到私有部分 并确保通过 getter 和 setter 进行干净的访问。幸运的是,这种设计是一种合理的继承设计方法,因此即使您不需要它也值得使用。
场景三:编译器不同或编译选项不兼容
如果您使用不同的编译器或不兼容的编译选项,那么真正的问题就开始了:
- 你不能依赖双方对内存布局有相同理解的假设。所以成员的 read/write 可能出现在不同的位置;一团糟!这就是为什么那么多 DLL classes 没有数据成员的原因。许多人还使用 PIMPL idiom 来隐藏私有内存布局。但在这种继承情况下,PIMPL 与使用私有数据非常相似(
*this
将是指向私有实现的隐式指针)
- compiler/linker 使用 "mangled" 函数名。不同的编译器可能使用不同的修饰,并且不会理解彼此的符号定义(即尽管
SetOutputPipe()
存在,但客户端不会找到它)。这就是为什么大多数 DLL 将所有成员函数都作为虚拟函数的原因:这些函数是通过 vtable 中的偏移量调用的,幸运的是,它在实践中跨编译器使用相同的布局。
- 最后,不同的编译器可以使用不同的调用约定。但我认为在成熟平台上的实践中,这不应该是一个主要风险
在 关于使用不同编译器(也没有继承)的 DLL 的问题中,我提供了一些可能与此类混合场景相关的额外解释和参考。
再次使用 private
成员数据而不是受保护的会使您更安全。使用 extern "C"
链接说明符公开 getters/setters(无论是受保护还是 public)将避免非虚函数的名称重整问题。
最后备注:
在库中公开 classes 时,应格外小心地设计 class,将数据设为私有。无论您处于何种情况,这种良好做法都值得额外思考。
我写了一个基于管道和过滤器的架构。为了避免混淆,过滤器在我的代码中被称为 "Stages"。这是基本思想:
我希望其他开发人员能够实现他们自己的阶段 class,然后我可以将其添加到 运行 时已经存在的阶段列表中。
我已经阅读了一段时间,它们似乎对动态代码加载有限制。我当前的舞台 class 看起来像这样:
class Stage
{
public:
void Process();
const uint16_t InputCount();
const uint16_t OutputCount();
void SetOutputPipe(size_t idx, Pipe<AmplitudeVal> *outputPipe);
void SetInputPipe(size_t idx, Pipe<AmplitudeVal> *inputPipe);
protected:
Stage(const uint16_t inputCount, const uint16_t outputCount);
virtual void init() {};
virtual bool work() = 0;
virtual void finish() {};
protected:
const uint16_t mInputCount;
const uint16_t mOutputCount;
std::vector< Pipe<AmplitudeVal>* > mInputs;
std::vector< Pipe<AmplitudeVal>* > mOutputs;
};
AmplitudeVal 只是浮点数的别名。此 class 仅包含对其连接的管道的引用(mInputs 和 mOutputs),它不处理任何算法 activity。我想尽可能少地公开,以便于外部开发人员使用。现在这个 class 只依赖于管道头和一些基本的配置文件。大多数处理加载 DLL 的示例都建议 class 只有纯虚函数,几乎没有任何成员变量。我不确定我应该做什么。
我知道你想在 DLL 中有一个阶段,并让用户用户在你的 DLL 上派生他们的工作。
场景 1:消费者和 DLL 使用相同的编译器和相同的标准库构建
如果消费者使用相同的编译器、兼容的编译选项,并且双方使用相同的共享标准库(默认使用 MSVC),那么您的解决方案 应该按原样工作
(参见
场景 2:消费者和 DLL 使用相同的编译器但不同的库构建
如果一方使用不同的标准库或其链接选项(例如,如果您为 DLL 使用静态链接库,为消费者使用共享库),那么您必须确保所有对象始终 created/released 在同一侧(因为 DLL 和应用程序将各自使用自己的分配函数和不同的内存池)。
这将非常困难,因为:
- 数据的继承
- 虚析构函数
- 标准容器的存储管理(在 DLL 和消费者中会有所不同,尽管源代码给人的印象可能是相同的)
朝着正确方向迈出的第一步是 将所有数据隔离到私有部分 并确保通过 getter 和 setter 进行干净的访问。幸运的是,这种设计是一种合理的继承设计方法,因此即使您不需要它也值得使用。
场景三:编译器不同或编译选项不兼容
如果您使用不同的编译器或不兼容的编译选项,那么真正的问题就开始了:
- 你不能依赖双方对内存布局有相同理解的假设。所以成员的 read/write 可能出现在不同的位置;一团糟!这就是为什么那么多 DLL classes 没有数据成员的原因。许多人还使用 PIMPL idiom 来隐藏私有内存布局。但在这种继承情况下,PIMPL 与使用私有数据非常相似(
*this
将是指向私有实现的隐式指针) - compiler/linker 使用 "mangled" 函数名。不同的编译器可能使用不同的修饰,并且不会理解彼此的符号定义(即尽管
SetOutputPipe()
存在,但客户端不会找到它)。这就是为什么大多数 DLL 将所有成员函数都作为虚拟函数的原因:这些函数是通过 vtable 中的偏移量调用的,幸运的是,它在实践中跨编译器使用相同的布局。 - 最后,不同的编译器可以使用不同的调用约定。但我认为在成熟平台上的实践中,这不应该是一个主要风险
在
再次使用 private
成员数据而不是受保护的会使您更安全。使用 extern "C"
链接说明符公开 getters/setters(无论是受保护还是 public)将避免非虚函数的名称重整问题。
最后备注:
在库中公开 classes 时,应格外小心地设计 class,将数据设为私有。无论您处于何种情况,这种良好做法都值得额外思考。