如何调用通过使用dlopen动态加载共享对象创建的对象的方法

4
我创建了一个名为libmathClass.so的测试库,将从下面的代码中加载它。这个共享对象有一个类和库调用被创建来返回这个类的对象。 我如何从下面的主要代码中调用这个对象的方法。由于链接器不知道方法的定义,所以我会从ld(链接器)中得到未定义的引用错误。
void* handle;
handle=dlopen("math1/libmathClass.so", RTLD_LAZY);
if(!handle)
{
    cout<<"error loading library: "<<dlerror()<<endl;
    exit(2);
}
else
{
    cout<<"***libmathClass.so library load successful!"<<endl;
}

void* (*mathInit) ();
mathInit = (void* (*)())dlsym(handle, "CreateMathOperationInstance");
if(!mathInit)
{
    cout<<"error loading instance method: "<<dlerror()<<endl;
    exit(3);
}
else
{
    cout<<"***method load successful!"<<endl;
}

mathOperationClass *mathInstance;
auto obj = (*mathInit)();
if(!obj)
{
    cout<<"object is not created"<<endl;
    exit(4);
}
else
{
    cout<<"object created!!!"<<endl;
    mathInstance = reinterpret_cast<mathOperationClass *>(obj);
}


int num1 = atoi(argv[1]);
int num2 = atoi(argv[2]);
cout<< mathInstance->AddInt(num1, num2)<<endl;

我使用的编译命令是 - g++ --std=c++11 -g -o dynamicTest dynamicMain.cpp -ldl

错误信息: dynamicMain.cpp:54: 对 `mathOperationClass::AddInt(int, int)' 的引用未定义 collect2: 错误:ld 返回 1


你需要链接到定义 mathInstance 的对象,以使用 AddInt() 方法。 - adrtam
你是否将函数名 CreateMathOperationInstance 导出为 extern "C"?如果没有,你需要提供名称的重载版本(不建议这样做)。 - Galik
我将CreateMathOperationInstance导出为extern "C",它只调用一个静态方法mathOperationClass::CreateInstance() { return new mathOperationClass(); } 并返回对象的指针。 - user2580634
只是一个快速的更新。显然,连接器不知道在运行时加载方法时调用的定义。这个问题的原始意图是想知道是否有其他方法可以使这个工作(调用在共享库中定义的对象方法)。如果上面程序中我调用的方法被定义为虚拟方法,那么这个特定的解决方案就会生效,因为这些方法将具有自己的虚表可供使用,并且链接器不会因为在运行时排序而抱怨未定义的引用。 虚表是C++的福音 :) - user2580634
1个回答

2
mathInit = (void* (*)())dlsym(handle, "CreateMathOperationInstance");

在这里,你使用dlsym()函数来查找共享库中的这个符号。这必须是一个具有C链接的函数,因为符号名称没有被重载。这一点很重要,在你看着这行代码时要记住:

cout<< mathInstance->AddInt(num1, num2)<<endl;

在这里,AddInt是由mathInstance指向的类的一个方法。类方法只是另一种函数,除了它始终需要一个隐藏的this指针作为额外参数。这就是类方法的意思,换句话说,在典型的C++实现中,实际上情况就是如此。严格来说,C++并不要求必须这样做。C++实现可以自由地以任何产生符合C++规范结果的方式来实现方法。但在实际情况下,在典型的C++实现中,类方法实际上就是带有额外参数的函数,该参数被引用为this
因此,某种程度上,上面的行与以下行基本等效:
cout<< mathOperationClass::AddInt(mathInstance, num1, num2)<<endl;

这基本上就是这里正在发生的事情,说得很粗略。这个mathOperationClass::AddInt 方法/函数,可以推测在你dlopen的同一共享库中。因为你使用了dlopen而没有实际链接它,所以你有了对此符号的引用。但是,由于无法在运行时解析此引用,因此您会遇到运行时未定义符号错误。
即使可能以这种方式调用此类方法,仅有的希望也是使用dlsym()。但是,为了能够进行这样的尝试,许多事情都需要恰到好处地发生。
首先,你必须弄清楚实际的C++符号名称。以我的Linux x86_64 g++编译器为参考,该方法的名称为 "_ZN18mathOperationClass6AddIntEii"。掌握了这个,你可以使用dlsym在你的共享库中找到此符号(或者你的C++实现中实际的名称为此方法的符号名)。
一旦你拥有了这个符号,接下来呢?那么,希望您的C++实现确实具有可黑客化的C++ ABI,可以通过显式传递额外的this参数来调用类方法,就像这样:
int (*addInt)(mathOperationClass *, int, int)=
    reinterpret_cast<int (*)(mathOperationClass *, int, int)>
        (dlsym(handle, "_ZN18mathOperationClass6AddIntEii"));

cout << (*addInt)(mathInstance, num1, num2) << endl;

如果不能确认在你的C++实现的ABI中可以通过这种hackish的方式调用C ++方法,整个卡牌屋将会崩塌。由于您已经使用了dlopen()函数,因此您已经处于非可移植性的领域,并且使用了特定于您的C++实现的资源,因此您可能需要确定您的C++方法是否可以以这种方式被调用。如果不能,您将不得不找出如何使用普通指针来调用它们。

现在我们来谈点不同的事情……

在上述所有内容中,有一种方法可以避免处理这个混乱局面:将这个类方法声明为虚拟类方法。虚拟类方法是通过内部虚拟函数表派发的。所以,只需将这个AddInt方法声明为虚拟类方法,并像往常一样调用它即可。由于编译器不会在这种情况下发出mathOperationClass :: AddInt的显式符号引用,因此在您的C++实现中很可能运行良好。它将通过自动附加到每个对象实例的虚拟函数表找到该方法。

当然,您还需要牢记虚拟函数的定义和影响。但是,在几乎所有情况下,这是一种相当便宜的调用动态加载自共享库类方法的方法。


AddInt 不是一个“普通指针”。它是一个方法调用。而且它不是通过给定的指针调用的。这个指针不是指向任何函数的指针,而是指向一个类的实例。如果你要反汇编你编译好的 C++ 代码,你会发现非虚拟方法调用等同于使用外部、混淆的符号引用调用外部函数。这就是所有现代 C++ 实现的工作方式。 - Sam Varshavchik
除非这个问题涉及到的代码明确地没有与共享库链接,而是通过dlopen打开的。 - Sam Varshavchik
啊,是的。我在等待那个“抓住你了”的时刻,现在出现了。我的调用方向是相反的(从插件到共享库),但这种情况正在呼唤插件。对不起,我现在明白你的意思了。 - Galik
AddInt是mathOperationClass类的一个常规公共访问方法。虚方法声明似乎是一个有效的解决方案,我会尝试一下。实际上,在编译期间尝试链接库,这很好用。我只有在dlopen共享库时才遇到这个问题。 - user2580634
我为使用虚拟方法的建议点了赞,这是我没有考虑到的。我的方法最初不是虚拟的,这就是链接器抱怨的原因。感谢Sam Varshavchik的建议,让我重新回顾了基础知识。 - user2580634

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接