2020年9月13日日曜日

Arduinoのライブラリでヘッダ(.h)ファイルに実装を書くと、エラーになることがある


背景

自分が作ったArduino向けのライブラリの中で、少ない記述だったのでヘッダファイルに関数の実装を書いていた部分がありました。
そのライブラリを1回しか呼び出さない場合は良かったのですが、Arduinoのライブラリの依存関係機能を利用して他のライブラリでも利用したところ、定義の重複エラーが発生しました。

エラーの回避方法を把握するのに時間がかかったのと、依存関係を利用して同様の問題に遭遇する人の助けになったら良いと思うので、備忘録を兼ねて記事を残します。

使ったもの

  • PlatformIOをインストールしたPC
    Arduino向けのプログラムをビルド出来る環境のひとつです。
    Arduino IDEよりライブラリのバージョン管理が楽なので、今回はこれを利用しました。
    Arduino IDEでは試していません。
  • ライブラリ: utils_asukiaaa
    自分がまとめているArduino向けの便利関数ライブラリです。
  • ライブラリ: I2cControlPanel_asukiaaa
    utils_asukiaaaを依存するライブラリとして呼び出している、コントローラ制御のためのライブラリです。

発生したエラーと、その際のプロジェクトの構成

utils_asukiaaa/wire.hをプロジェクトのmainファイルとI2cControlPanel_asukiaaaで使った際に、下記のようにutils_asukiaaa::wire::writeBytesが複数箇所で定義されているというエラーが発生していました。
Linking .pio/build/leonardo/firmware.elf
I2cControlPanel_asukiaaa.cpp.o (symbol from plugin): In function `utils_asukiaaa::wire::readBytes(TwoWire*, unsigned char, unsigned char, unsigned char*, unsigned char)':
(.text+0x0): multiple definition of `utils_asukiaaa::wire::readBytes(TwoWire*, unsigned char, unsigned char, unsigned char*, unsigned char)'
.pio/build/leonardo/src/main.cpp.o (symbol from plugin):(.text+0x0): first defined here
I2cControlPanel_asukiaaa.cpp.o (symbol from plugin): In function `utils_asukiaaa::wire::readBytes(TwoWire*, unsigned char, unsigned char, unsigned char*, unsigned char)':
(.text+0x0): multiple definition of `utils_asukiaaa::wire::writeBytes(TwoWire*, unsigned char, unsigned char, unsigned char*, unsigned char)'
.pio/build/leonardo/src/main.cpp.o (symbol from plugin):(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
*** [.pio/build/leonardo/firmware.elf] Error 1

エラーが出る最小構成のプログラムはこちらです。
utils_asukiaaaのバージョン1.0.2の書き方に問題があったので、この記述でエラーを再現できます。
src/main.cpp
#include <Arduino.h>
#include <utils_asukiaaa.h>
#include <utils_asukiaaa/wire.h>
#include <I2cControlPanel_asukiaaa.h>

I2cControlPanel_asukiaaa controller;

void setup() {
}

void loop() {
}
platformio.ini
[env:leonardo]
platform = atmelavr
board = leonardo
framework = arduino
lib_deps =
https://github.com/asukiaaa/I2cControlPanel_asukiaaa.git#1.1.0
https://github.com/asukiaaa/utils_asukiaaa.git#1.0.2

場合によってエラーが発生するライブラリのコードとその原因

utils_asukiaaaのバージョンv1.0.2のwire.hのコードです。
定義も実装もヘッダファイルにまとめていました。
src/utils_asukiaaa/wire.h
#ifndef _UTILS_ASUKIAAA_WIRE_H_
#define _UTILS_ASUKIAAA_WIRE_H_

#include <Arduino.h>
#include <Wire.h>

namespace utils_asukiaaa {
namespace wire {
int readBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen) {
wire->beginTransmission(deviceAddress);
wire->write(registerAddress);
uint8_t result = wire->endTransmission();
if (result != 0) {
return result;
}

wire->requestFrom(deviceAddress, dataLen);
uint8_t index = 0;
while (wire->available()) {
uint8_t d = wire->read();
if (index < dataLen) {
data[index++] = d;
}
}
return 0;
}

int writeBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen) {
wire->beginTransmission(deviceAddress);
wire->write(registerAddress);
wire->write(data, dataLen);
return wire->endTransmission();
}
}
}

#endif

上記の関数の実装を含むヘッダファイル(utils_asukiaaa/wire.h)を別のライブラリであるI2cControlPanel_asukiaaaとプロジェクトのmain.cppで読み込むことにより、それぞれビルドした後に統合する際に定義が重複してしまうのが原因のようでした。

具体的にはヘッダファイル(utils_asukiaaa/wire.h)にある実装がmain.cpp.oとI2cControlPanel_asukiaaa.cpp.oでビルドされることにより、それらの.oファイルをを統合する際にエラーが発生するようです。
Compiling .pio/build/leonardo/src/main.cpp.o
Compiling .pio/build/leonardo/libfea/I2cControlPanel_asukiaaa/I2cControlPanel_asukiaaa.cpp.o

エラーが発生しないライブラリのコード

対応1: ファイル名を.hから.hppにする

.hファイルは実装を含まない定義だけを想定しているため読み込みの重複が発生しますが、.hppファイルは定義と実装を含むファイルを意味するためエラーが発生しなくなります。

hとcppファイルに分けるのが手間だったり、そうするのが難しい場合(template利用時など)は、hppファイルとして定義と実装を1つのファイルにまとめるのが良いと思います。

対応2: 定義と実装をhとcppに分ける

utils_asukiaaaのv1.0.3のwire.hwire.cppのコードです。
ヘッダファイルは関数の定義だけにして、実装はcppファイルに書きました。
src/utils_asukiaaa/wire.h
#ifndef _UTILS_ASUKIAAA_WIRE_H_
#define _UTILS_ASUKIAAA_WIRE_H_

#include <Arduino.h>
#include <Wire.h>

namespace utils_asukiaaa {
namespace wire {
int readBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen);
int writeBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen);
}
}

#endif
src/utils_asukiaaa/wire.cpp
#include "utils_asukiaaa/wire.h"

namespace utils_asukiaaa {
namespace wire {
int readBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen) {
wire->beginTransmission(deviceAddress);
wire->write(registerAddress);
uint8_t result = wire->endTransmission();
if (result != 0) {
return result;
}

wire->requestFrom(deviceAddress, dataLen);
uint8_t index = 0;
while (wire->available()) {
uint8_t d = wire->read();
if (index < dataLen) {
data[index++] = d;
}
}
return 0;
}

int writeBytes(TwoWire *wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t* data, uint8_t dataLen) {
wire->beginTransmission(deviceAddress);
wire->write(registerAddress);
wire->write(data, dataLen);
return wire->endTransmission();
}
}
}

#endif

上記の定義だけが記述されたヘッダファイルを利用することで、エラーが発生していたプロジェクトが問題なくビルドできるようになりました。

再現したい場合はutils_asukiaaaのバージョンを1.0.3以上にするとビルドが成功します。
platformio.ini
[env:leonardo]
platform = atmelavr
board = leonardo
framework = arduino
lib_deps =
https://github.com/asukiaaa/I2cControlPanel_asukiaaa.git#1.1.0
https://github.com/asukiaaa/utils_asukiaaa.git#1.0.3

まとめ

ライブラリとして分割して複数回呼び出される可能性がある場合は、ヘッダファイルに関数の実装を記述していると、今回のような定義の重複に関するエラーが発生することがあるようでした。

プロジェクトのmainファイル分割してmainからしか呼び出されないローカルなファイルならヘッダファイルに実装を記述しても問題無いと思いますが、ライブラリとして分割する場合はひと手間かけておくと今回のようなエラーを回避できて良さそうです。

変更履歴

2021.03.21
I2cControlPanelの更新に伴い紹介しているコードではエラーが再現しなくなったため、githubから該当するバージョンをダウンロードするlib_depsの形式に変更しました。
ファイル名を.hppに変更すればエラーが発生しなくなったため、その対応方法を追記しました。

0 件のコメント :