英文:
multiple C headers included in C++ project
问题
In your C++ (C++17) project, you're facing issues with multiple C headers having the same guards, and you can't modify the C files. To manage this problem using CMake and the preprocessor, you can consider the following approach:
-
Use Separate Folders for Include Files: Organize the embedded device headers in separate folders within your C++ project, such as "device_01" and "device_02."
-
Create a Wrapper Header for Each Device: In each device-specific folder, create a wrapper header file (e.g., "device_01_wrapper.h" and "device_02_wrapper.h"). Inside these wrapper headers, you can include the corresponding embedded device header.
-
Use CMake to Set Include Directories: In your CMakeLists.txt file, set include directories for each device-specific folder using
target_include_directories
. This will ensure that the wrapper headers are included correctly. -
Include the Wrapper Headers: In your C++ code, include the wrapper headers (e.g., "device_01_wrapper.h" and "device_02_wrapper.h") instead of the original embedded device headers.
This approach allows you to avoid conflicts with header guards, as each wrapper header can have unique guards. It also encapsulates the device-specific headers within their respective folders.
Here's an example of how your CMakeLists.txt might look:
add_library(Device01Driver device_01_driver.cpp)
target_include_directories(Device01Driver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/_deps/device_01-src/src)
target_include_directories(Device01Driver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/devices/device_01)
add_library(Device02Driver device_02_driver.cpp)
target_include_directories(Device02Driver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/_deps/device_02-src/src)
target_include_directories(Device02Driver PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/devices/device_02)
And in your C++ code, you can include the wrapper headers:
#include "devices/device_01/device_01_wrapper.h"
#include "devices/device_02/device_02_wrapper.h"
// Your code that uses Device01 and Device02
This way, you can avoid conflicts and effectively manage the inclusion of device-specific headers in your C++ project.
英文:
I have C++ (C++17) project (let's call it "Client") where I have to connect with multiple different embedded devices.
Software for each device is written in C99 standard. Each device project has a directory like "config/" with
a header like "states.h" where are described the states of the state machine (also some types, etc.).
In my C++ (client) project I'm fetching each of the embedded device directories as a local repo, using CMake.
The problems are:
- I have multiple C headers with the same guards
#ifndef _STATES_H_
and when I compile the C++ project only one file will be
......
taken into account (obvious). - Even if I can change the guard's name (
_EMBDEVICE_1_STATES_H
,_EMBDEVICE_2_STATES_H
) still (probably)
the C++ compiler will not include two files with the same name. - Even if can solve both above problems, in each of these files I have
struct and types with the same name (for exampletypedef struct
etc).
state_mahine {....}
The major problem is I CAN'T modify the C files.
If I could, I could change the guard's name, I can surround content with the struct struct embdevice_1 { ... };
but still is the problem B) - I have two files with the same name.
If it's possible, how can I manage this problem using CMake, and/or preprocessor?
EDIT (19.04.2023)
The source code of my embedded devices I'm fetching by FetchContent_Declare
.
My project tree looks like:
my_project
├─ CMakeLists.txt
├─ _deps
│ ├─ device_01-src
│ │ ├─ src
│ │ └─ state_mahine
│ │ ├─ states.h
│ │ └─ config.h
│ └─ device_02-src
│ ├─ src
│ └─ state_mahine
│ ├─ states.h
│ └─ config.h
│
├─ devices
│ ├─ Device_01_Driver <-- only here including the headers from device_01
│ │ ├─ CMakeLists.txt <-- only here I'm fetching the device_01 repo
│ │ ├─ device_01_driver.hpp
│ │ ├─ device_01_driver.cpp
│ ├─ Device_02_Driver <-- only here including the headers from device_02
│ │ ├─ CMakeLists.txt <-- only here I'm fetching the device_02 repo
│ │ ├─ device_02_driver.hpp
│ │ ├─ device_02_driver.cpp
│ ├─ devices.hpp
├─ main.cpp
├─ main.hpp
I'm including the files from device_x_src
repository only from device_x_
drivers files, and in main
I'm including the devices.hpp
and drivers class (my further goal is no need to include drivers in main
, but only in the devices.hpp
, however, this is not the point of my problem now). So basically all of the structures etc. are encapsulated in the driver cpp files, and there's no interconnection between C repositories.
I'm not a CMake master, but maybe is a possibility to reduce the scope of #define (workaround of _GUARDS__H) and variables (some workaround like using the namespaces)
答案1
得分: 1
根据您提供的内容,以下是翻译好的部分:
"From your description plus your answers in the comments, there are two components to this:
- Actually including the files depending on their name.
- Utilizing the files in your code."
首先,要实际包括文件,您可以始终使用显式路径引用它们。例如,如果您的设备具有以下目录结构(可以通过例如 git 子模块实现):
src
yoursourcefiles.cpp
device1
config
state.h
somethingelse.c
device2
config
state.h
anotherthingelse.c
- ...
那么您可以使用相对路径明确地包含配置文件:
#include "../device1/config/state.h
(对于 device1
),#include "../device2/config/state.h
(对于 device2
),依此类推。
至于第二部分:既然您说您不能修改 C 文件本身,并且它们使用相同的包含保护符号并定义相同的类型名称,以下是一种可能的方法来实现这一点,但绝不是唯一的方法。
# 包含在单独文件中的变体
首先,您需要定义某种调度机制。在这种情况下,我将展示一个封装了特定设备功能的抽象基类的示例。例如,如下所示:
class Device {
public:
virtual ~Device() {}
virtual std::string readSerialNumber() const = 0;
};
(我不知道您需要什么特定功能,所以我将其限制为 readSerialNumber()
作为示例。)
然后,您需要为每个设备类型的实例化函数定义一些原型,以及每个个体设备的定义。在这种情况下,我选择使用模板:
enum class DeviceType {
Device1,
Device2,
// ...
};
template<DeviceType deviceType>
std::unique_ptr<Device> instantiateDeviceImpl();
// 预先声明所有设备类型的实现:
extern template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device1>();
extern template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device2>();
// 通用调度程序
inline std::unique_ptr<Device> instantiateDevice(DeviceType deviceType)
{
switch (deviceType) {
case DeviceType::Device1: return instantiateDeviceImpl<DeviceType::Device1>();
case DeviceType::Device2: return instantiateDeviceImpl<DeviceType::Device2>();
default: throw std::runtime_error("Invalid device type");
}
}
# 子变体:设备之间没有共享代码
如果公共类的每个实现实际上没有共享代码,您可以简单地为每个设备单独定义不同的 C++ 实现。在这种情况下,您将创建一个文件 device1_impl.cpp
,内容如下:
// 包括 Device 类的定义
#include "device.hpp"
// 使用匿名命名空间以确保设备特定头文件中定义的数据结构
// 仅在此独立源文件中可见,使用 extern “C”,因为 C 头文件可能不包括它
namespace {
extern “C” {
// 包括设备特定头文件
#include "../device1/config/state.h";
}
class Device1Impl : public Device {
public:
virtual ~Device1Impl() {}
virtual std::string readSerialNumber() const
{
// 使用在头文件中定义的数据的代码
}
};
template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device1>()
{
return std::make_unique<Device1Impl>();
}
还要为其他设备类型创建类似的文件。这允许您重用设备的 C 头文件。
# 子变体:所有 C++ 代码在设备类型之间共享
在这种情况下,您将使用不同的方法处理 device1_impl.cpp
的内容:
// 定义可供通用代码使用的宏
#define IMPL_DEVICE_TYPE Device1
// 使用匿名命名空间以确保设备特定头文件中定义的数据结构
// 仅在此独立源文件中可见,使用 extern “C”,因为 C 头文件可能不包括它
namespace {
extern “C” {
// 包括设备特定头文件
#include "../device1/config/state.h";
}
}
// 现在包括通用代码
#include "device.impl.hpp";
然后,device.impl.hpp
文件可以具有以下内容(从技术上讲,它不是头文件,但如果将其命名为 .cpp
,然后将其添加到构建系统中,它将尝试自行编译该文件,这不是太好 - 最好将其命名为 .hpp
):
#include "device.hpp";
// 定义匿名命名空间以避免 ODR 违规
namespace {
class DeviceImpl : public Device {
public:
virtual ~DeviceImpl() {}
virtual std::string readSerialNumber() const
{
// 使用在头文件中定义的数据的代码
}
};
}
template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::IMPL_DEVICE_TYPE>()
{
// 这将使用特定设备类型的本地 DeviceImpl
return std::make_unique<DeviceImpl>();
}
# 变体:在同一文件中包含所有 C 头文件
以下内容可能或可能不适用,这取决于 C 头文件中定义了什么数据结构。您可以使用不同的 C++ 命名空间并在各种命名空间中包含文件:
// 使用显式定义的命名空间以确保没有名称冲突,使用 extern “C”,因为 C 头
<details>
<summary>英文:</summary>
From your description plus your answers in the comments, there are two components to this:
1. Actually including the files depending on their name.
2. Utilizing the files in your code.
First of all, to actually include the files, you can always reference them with explicit paths. So for example, if your devices have the following directory structure (this could be achieved via e.g. git submodules):
- `src`
- `yoursourcefiles.cpp`
- `device1`
- `config`
- `state.h`
- `somethingelse.c`
- `device2`
- `config`
- `state.h`
- `anotherthingelse.c`
- ...
Then you can include the configuration files explicitly using relative paths:
`#include "../device1/config/state.h` (for `device1`), `#include "../device2/config/state.h` (for `device2`), etc.
As for the second part: Since you said you can't modify the C files themselves and they use the same include guard + define the same type names, the following would be one *possible* method of achieving this, but by no means the own.
# Variants that include the headers in separate files
First you'll need to define some kind of dispatch mechanism. In this case I'll showcase this for an abstract base class that encapsulates the device-specific functionality. For example something like the following:
``` c++
class Device {
public:
virtual ~Device() {}
virtual std::string readSerialNumber() const = 0;
};
(I have no idea what specific functionality you need, so I've restricted this to readSerialNumber()
as an example.)
Then you'll need to have some kind of prototype for an instantiation function for each device type, plus definitions for each individual device. In this case I've chosen to use a template:
enum class DeviceType {
Device1,
Device2,
// ...
};
template<DeviceType deviceType>
std::unique_ptr<Device> instantiateDeviceImpl();
// Forward-declare all implementations for all device types:
extern template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device1>();
extern template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device2>();
// Generic dispatcher
inline std::unique_ptr<Device> instantiateDevice(DeviceType deviceType)
{
switch (deviceType) {
case DeviceType::Device1: return instantiateDeviceImpl<DeviceType::Device1>();
case DeviceType::Device2: return instantiateDeviceImpl<DeviceType::Device2>();
default: throw std::runtime_error("Invalid device type");
}
}
Sub-variant: no common code between devices
If each of the implementations of the common class don't actually share code you can simply define different C++ implementations for each device individually. In that case you'd create a file device1_impl.cpp
with content like the following:
// Include the definition of the Device class
#include "device.hpp"
// Use an anonymous namespace to ensure that the data
// structures defined in the device-specific header are
// only visible to this individual source file, and use
// extern "C" because the C headers likely don't include
// that.
namespace {
extern "C" {
// Include the device specific header
#include "../device1/config/state.h"
}
class Device1Impl : public Device {
public:
virtual ~Device1Impl() {}
virtual std::string readSerialNumber() const
{
// Code that makes use of the data defined in the header
}
};
template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::Device1>()
{
return std::make_unique<Device1Impl>();
}
Also create the analogous files for the other device types. This allows you to reuse the C header files of the devices.
Sub-variant: all C++ code is shared between device types
In that case you'll want to use a different approach with the contents of device1_impl.cpp
:
// Define a macro that can be used by the generic code
#define IMPL_DEVICE_TYPE Device1
// Use an anonymous namespace to ensure that the data
// structures defined in the device-specific header are
// only visible to this individual source file, and use
// extern "C" because the C headers likely don't include
// that.
namespace {
extern "C" {
// Include the device specific header
#include "../device1/config/state.h"
}
}
// Now include the generic code
#include "device.impl.hpp"
The file device.impl.hpp
could then have the following contents (it's technically not a header, but if you call it .cpp
then if you add it to the build system it will attempt to compile that file on its own, which isn't great - better to call it .hpp
):
#include "device.hpp"
// Define an anonymous namespace to avoid ODR violations
namespace {
class DeviceImpl : public Device {
public:
virtual ~DeviceImpl() {}
virtual std::string readSerialNumber() const
{
// Code that makes use of the data defined in the header
}
};
}
template
std::unique_ptr<Device> instantiateDeviceImpl<DeviceType::IMPL_DEVICE_TYPE>()
{
// This will use the DeviceImpl local to the specific device type
return std::make_unique<DeviceImpl>();
}
Variant: Include all C headers in the same file
The following may or may not work, depending on what data structures are defined in the C headers. You could use different C++ namespaces and include the files within the various namespaces:
// Use explicitly defined namespaces to ensure that there
// are no name collisions, and use extern "C" because the
// C headers likely don't include that.
namespace device1 {
extern "C" {
// Undefine the header's guard macro
#ifdef _STATES_H_
#undef _STATES_H_
#endif
#include "../device1/config/state.h"
}
} // namespace device1
namespace device2 {
extern "C" {
// Undefine the header's guard macro
#ifdef _STATES_H_
#undef _STATES_H_
#endif
#include "../device2/config/state.h"
}
} // namespace device2
Then in the source file you can use all of the various names at once, i.e.
device1::struct_type_name
for the first device type, device2::struct_type_name
for the second device type, etc.
I would not recommend this variant, because if the various device-specific state.h
headers themselves include other headers again, then you'd potentially also have to undefine those other header's guard macros, unless those headers are the same for all devices. Also you'll need to undefine any macro that the device-specific headers define themselves before including the next one (and your code may not rely on any of these macros, unless they are identical for all of the devices).
Caution
Some additional word of caution here: if the C headers include some data types that are different on the device and on your system (e.g. int
if often 16bit on microcontrollers, while it's typically 32bit on most desktop systems), then you'll run into issues. But even if the types themselves are the same, the alignment requirements and/or the endianness may still be different, depending on what exactly it is you're using. So be careful here.
Side-note: device-specific repositories not subdirectories of your project
If the device source code is not a subdirectory of your project directly, but you're using CMake's find_package()
functionality to find where the source files are located, you could export the include path of the device-specific projects with device-specific names (via e.g. @cmakedefine
in a generated header + doing the include via #include DEVICE1_INC_PATH "/config/state.h"
, where DEVICE1_INC_PATH
would be the same of the macro you define via the build system).
Summary
I hope this gives you some ideas on how what you're looking for can be achieved. There are of course other mechanisms you can use to perform the dispatching to different device types, the abstract base class is just a (relatively simple) method of doing this. It will depend strongly on your specific use case what abstraction is the best here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论