C++ ld链接器--wrap选项对内部函数调用无效

3
我正在尝试为一个不使用面向对象的C++库实现一些单元测试(所有函数都声明在命名空间级别)。
为此,我正在创建一个测试二进制文件,模拟一些函数。
我已经成功地为我直接调用的函数实现了上述功能,但是我无法替换库函数所调用的函数。下面的示例说明了这一点:

生产代码

假设下面是生产代码,它使用真实函数而不是模拟函数: CameraHandler.H
namespace Cam {
    int myFunc();
    int myFunc2();
}

CameraHandler.cpp

#include "CameraHandler.h"

using namespace Cam;

int Cam::myFunc() {
    // Imagine this is the function I want to simulate with a mock
    // Its mangled name is _ZN3Cam6myFuncEv
    return 1;
}

int Cam::myFunc2(){
    return Cam::myFunc() + 11;
}

测试代码

这是单元测试的代码。如您在Makefile中所见,它生成一个名为testsMain的二进制文件。

CameraHandlerMock.h

extern "C" {
    int __wrap__ZN3Cam6myFuncEv(); // mangled name of Cam::myFunc(), with the __wrap_ prefix.
}

CameraHandlerMock.cpp

#include "CameraHandlerMock.h"

int __wrap__ZN3Cam6myFuncEv(){
    // As you can see, the mocked function returns 999 instead of 1.
    return 999;
}

UnitTestsMain.cpp

#include <iostream>
#include <typeinfo>
#include "CameraHandler.h"
#include "CameraHandlerMock.h"

extern "C" int _ZN3Cam6myFuncEv();

int main(){
    std::cout << Cam::myFunc() << std::endl;
    std::cout << Cam::myFunc2() << std::endl;
    return 0;
}

Makefile 文件

WRAP=-Wl,--wrap,_ZN3Cam6myFuncEv

all: production unitTests

production: // does not matter for this example
        g++ main.cpp CameraHandler.cpp -o main 

unitTests:
        g++ ${WRAP} UnitTestsMain.cpp CameraHandlerMock.cpp CameraHandler.cpp -o testsMain

问题

如果我执行testsMain程序,将会得到以下结果:

999 // call to Cam::myFunc()
12 // Cam::myFunc2(), which is Cam::myFunc() + 11.

考虑到Cam::myFunc2()调用了Cam::myFunc1(),而我已经将其替换为__wrap__ZN3Cam6myFuncEv,所以我期望调用Cam::myFunc2()的结果是999 + 11 = 1010。然而,Cam::myFunc2()仍然调用未包装的Cam::myFunc1(),因此结果为12是否有办法包装由我想要测试的库内部调用的函数?
1个回答

3

让我们先来检查一下一些不必要的代码。在 UnitTestsMain.cpp 文件中,有以下声明:

extern "C" int _ZN3Cam6myFuncEv();

是多余的。它只是告诉C++编译器,对那个原型函数的引用,其mangled名为_ZN3Cam6myFuncEv,是对该名称的外部定义函数的引用。这与编译器已经从以下代码中获得的信息完全相同,只是表达方式不同:

namespace Cam {
    int myFunc();
    ...
}

当包含CameraHandler.h时,因为_ZN3Cam6myFuncEv()Cam::myFunc的名称修饰形式。对Cam::myFuncextern "C"重新声明没有害处,但对编译或链接也没有任何贡献。

接下来是主要问题:为什么在UnitTestsMain.cpp中会调用您的模拟函数int __wrap__ZN3Cam6myFuncEv()而不是int Cam::myFunc

int main(){
    std::cout << Cam::myFunc() << std::endl;
    std::cout << Cam::myFunc2() << std::endl;
    return 0;
} 

按照你的要求;但是你的模拟测试没有在CameraHandler.cpp文件中调用int Cam::myFunc函数:

int Cam::myFunc2(){
    return Cam::myFunc() + 11;
}

答案在链接器选项--wrap文件中的说明中:

--wrap=symbol

使用符号的包装函数。对于任何对该符号的未定义引用,将解析为__wrap_symbol。对于任何对__real_symbol的未定义引用,将解析为symbol。

也许你读了它,但没有理解未定义引用的重要性。
这意味着当有效应--wrap=symbol时,并且链接器将其应用于包含对symbol未定义引用的对象文件时,它将使用对__wrap_symbol的引用来替换它们,并且在该对象文件中,对__real_symbol未定义引用将被替换为symbol
现在,在从UnitTestsMain.cpp编译而来的UnitTestsMain.o中,对Cam::myFunc()Cam::myFunc2()的引用都是未定义的。这两个函数都定义在CameraHandler.cpp中,编译为CameraHandler.o
因此,在链接UnitTestsMain.o时,--wrap ZN3Cam6myFuncEv将生效,并将调用Cam::myFunc(=ZN3Cam6myFuncEv)替换为调用__wrap_ZN3Cam6myFuncEv。对于Cam::myFunc2()(=ZN3Cam7myFunc2Ev)的调用没有使用包装,不受影响:它将被解析为在CameraHandler.o中找到的定义。
但是,在链接CameraHandler.o时,两个函数都被定义了,因此--wrap不起作用。当Cam::myFunc2()调用Cam::myFunc()时,它调用ZN3Cam6myFuncEv而不是__wrap_ZN3Cam6myFuncEv
这就解释了为什么程序输出:
999
12  

不是:

999
1010

你能让你的嘲弄按预期工作吗?

可以。您只需确保每次调用 Cam::myFunc 并且希望进行模拟的调用都编译为不包含(真实)定义 Cam::myFunc 的对象文件。显而易见的方法是在其自己的源文件中定义 Cam::myFunc。以下是修正后的示例:

CameraHandler.h

#ifndef CAMERAHANDLER_H
#define CAMERAHANDLER_H

namespace Cam {
    int myFunc();
    int myFunc2();
}

#endif

CameraHandlerMock.h

#ifndef CAMERAHANDLERMOCK_H
#define CAMERAHANDLERMOCK_H

extern "C" {
    int __wrap__ZN3Cam6myFuncEv();

}

#endif

CameraHandler_myFunc.cpp

#include "CameraHandler.h"

using namespace Cam;

int Cam::myFunc() {
    return 1;
}

CameraHandler_myFunc2.cpp

#include "CameraHandler.h"

using namespace Cam;

int Cam::myFunc2(){
    return Cam::myFunc() + 11;
}

CameraHandlerMock.cpp

#include "CameraHandlerMock.h"

int __wrap__ZN3Cam6myFuncEv() {
    return 999;
}

UnitTestsMain.cpp

#include <iostream>
#include "CameraHandler.h"
#include "CameraHandlerMock.h"

int main(){
    std::cout << Cam::myFunc() << std::endl;
    std::cout << Cam::myFunc2() << std::endl;
    return 0;
}

Makefile

SRCS := UnitTestsMain.cpp CameraHandler_myFunc.cpp \
    CameraHandler_myFunc2.cpp CameraHandlerMock.cpp
OBJS := $(SRCS:.cpp=.o)

LDFLAGS := -Wl,--wrap,_ZN3Cam6myFuncEv

.PHONY: unitTests clean

unitTests: testsMain

testsMain: $(OBJS)
    $(CXX) $(LDFLAGS) -o $@ $^

UnitTestsMain: CameraHandler.h CameraHandlerMock.h
CameraHandler_Func.o CameraHandler_Func2.o: CameraHandler.h
CameraHandlerMock.o: CameraHandlerMock.h

clean:
    rm -f $(OBJS) testsMain

在此示例的Makefile中,您的生产版本构建根本没有被考虑。
通过这种方式,测试构建的运行如下:
$ make
g++    -c -o UnitTestsMain.o UnitTestsMain.cpp
g++    -c -o CameraHandler_myFunc.o CameraHandler_myFunc.cpp
g++    -c -o CameraHandler_myFunc2.o CameraHandler_myFunc2.cpp
g++    -c -o CameraHandlerMock.o CameraHandlerMock.cpp
g++ -Wl,--wrap,_ZN3Cam6myFuncEv -o testsMain UnitTestsMain.o \
CameraHandler_myFunc.o CameraHandler_myFunc2.o CameraHandlerMock.o

而且testsMain会按照你的期望进行操作:

$ ./testsMain 
999
1010

如果你将CameraHandlerMock.cpp重写为以下内容,你可以简化源文件和makefile:

extern "C" {

int __wrap__ZN3Cam6myFuncEv() {
    return 999;
}

}

那么,您根本不需要模拟头文件CameraHandlerMock.h
如果您有许多需要以这种低级方式进行模拟的功能,则为每个功能单独定义可能会变得乏味。您可能知道,有更高级别、框架支持的模拟选项,例如googlemock,具有丰富的模拟功能,而不需要进行这种乏味的工作。但是,可以说,它们可能用更复杂的乏味代替它。

1
是的,我已经在使用GoogleMock进行C++单元测试。我写这篇文章是因为我想要一种方法来模拟命名空间级别的函数,据我所知,你不能直接使用GoogleMock来模拟它们,因为你需要注入一个接口。 - Dan
1
这是我没有考虑到的支持这种方式的一个观点。值得回答3个月前的问题 :) - Mike Kinghan

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