Arduino TexitStringライブラリのメモリーリーク
メールチェッカーがメモリーリークを起こしている原因がやっと分かりました。犯人はやはりTextStringライブラリでした。
確認用スケッチ
以下のスケッチを実行することでメモリーリークが確認できます。テストコードはメールチェッカーのStringクラスを使用した文字列操作部分を抜粋しています。Stack/Heapトップの取得方法(check_mem関数)は、Arduino Playground記載のコードを流用しました。
#include <WString.h>
#define BufferLength 128
String ReceiveBuffer(BufferLength + 2);
int Count = 0;
void setup()
{
Serial.begin(115200);
check_mem();
ReceiveBuffer = "+OK 14 327647"; //ダミーで固定値を入れておく
}
void loop()
{
Serial.print("Count=");
Serial.println(Count, DEC);
checkString();
Count++;
check_mem();
Serial.println();
delay(5000);
}
void checkString()
{
int firstSpace = ReceiveBuffer.indexOf(" ", 0);
int secondSpace = ReceiveBuffer.indexOf(" ", firstSpace+1);
String numberOfMails = ReceiveBuffer.substring(firstSpace+1, secondSpace );
int lastNumMails = atoi((char*)numberOfMails);
Serial.println(lastNumMails);
}
uint8_t * heapptr, * stackptr;
void check_mem()
{
stackptr = (uint8_t *)malloc(4); // use stackptr temporarily
heapptr = stackptr; // save value of heap pointer
free(stackptr); // free up the memory again (sets stackptr to 0)
stackptr = (uint8_t *)(SP); // save value of stack pointer
Serial.print("Heap ptr :");
Serial.println((long)heapptr, HEX);
Serial.print("Stack ptr:");
Serial.println((long)stackptr, HEX);
}
実行結果は以下の通りです;
Count=0
Heap ptr :287
Stack ptr:8F5
Count=1
Heap ptr :296
Stack ptr:8F5
Count=2
Heap ptr :2A5
Stack ptr:8F5
Count=3
Heap ptr :2B4
Stack ptr:8F5
Count=4
Heap ptr :2C3
Stack ptr:8F5
ループが回る毎に、16byte (0xF)Heap Pointerが増加している、即ち解放されないメモリー領域がヒープエリアに蓄積していることが分かります。Heap ptrがStack ptrに届いたときに、プログラムがクラッシュしてしまいます。
どこでメモリーリークが起きているのか
問題になるStringライブラリのメンバー関数は以下です;
1)int firstSpace = ReceiveBuffer.indexOf(" ", 0);
2)String numberOfMails = ReceiveBuffer.substring(firstSpace+1, secondSpace );
2)のsubstring()メンバー関数については、以下のライブラリソースに問題がありそうです。substring()は、文字列の指定範囲を切りだして、切りだした部分をStringオブジェクトとして呼び出し側に返します。戻り値となるStringオブジェクトを関数内で宣言していますが(赤字の部分)、このオブジェクトが解放されずに残ってしまうのだと思います。
String String::substring(int beginIndex, int endIndex)
{
if ( beginIndex > endIndex )
{
int tmp = endIndex;
endIndex = beginIndex;
beginIndex = tmp;
}
if ( endIndex > _length )
{
exit(1);
}
char ch = _array[ endIndex ];
_array[ endIndex ] = '\0';
String str = String( _array + beginIndex );
_array[ endIndex ] = ch;
return str;
}
1)のindexOf()メンバー関数については、関数内でStringオブジェクトを新たに確保していることはありません。こちらは、呼び出し側で引数として指定した「" "」がStringオブジェクトとして生成され削除されずに残ってしまうものと思われます。
int String::indexOf(const String &str, int fromIndex)
{
if(fromIndex >= _length)
return -1;
char *result = strstr(&_array[fromIndex], str.cstr());
if(result == NULL)
return -1;
return result - _array;
}
対策は
そもそも、関数の戻り値としてオブジェクトを確保した場合、そのオブジェクトはどこで・誰が廃棄するの?
答えは、デストラクタを使うことで、Arduino Forumに回答がありました。C++は上記をスマートに解決する方法があったのですね。「Cとは違うのだよ、Cとは」とC++コンパイラさんにあざ笑われたような、、
Stringライブラリのコードに、以下のデストラクタを追加することで問題が解決しました。
<WString.hへの追加>
public:
~String(); ←追加
<WString.cppへの追加>
String::~String()
{
free(_array);
}
上記のデストラクタを追加することによって、オブジェクトのスコープを抜けた際に、文字列格納格納領域(_array)を解放するようになりメモリーリークが解消しました。
メモリーリーク対策後のテストスケッチ実行結果
デストラクタを追加し、WString.oファイルを削除してから、テストスケッチを再コンパイルします。「warning: comparison between signed and unsigned integer」といった警告が一杯出るのですが、ビルドに成功すればOKとします。
実行結果は以下となり、heap pointerがピクリとも動かなくなりました。
Count=0
Heap ptr :278
Stack ptr:8F5
Count=1
Heap ptr :278
Stack ptr:8F5
Count=2
Heap ptr :278
Stack ptr:8F5
Count=3
Heap ptr :278
Stack ptr:8F5
Count=4
Heap ptr :278
Stack ptr:8F5
C++のデストラクタは、名前を知っている程度で、有用な利用シーンを見いだしたことがなかったのですが、こういう使い方があるのですね。
問題となった2)のケースでは、substring()終了時に、呼び出し元のString numberOfMailsにオブジェクトをコピーしてからsubstring()内で生成したStringオブジェクトをデストラクタが削除してくれるのだと思います。
う~、C++のメモリー管理は複雑だ、、
実は、Arduinoに触るまではC++をまともに使ったことがなかったのです。オブジェクト指向言語としては、C#なら少々触ったことがあるのですが、こちらはガベージコレクタが自動的に不要メモリ領域の回収行ってくれるので楽ちんですよね。
« ArduinoでNTPを使用する | トップページ | Arduinoメールチェッカー(その3) »
「Arduino」カテゴリの記事
- GLCDシールドの制作(2009.07.21)
- Arduino Ethernet Shieldのパワーオンリセット(2010.06.13)
- Arduino RSSリーダー(2009.09.06)
- LiquidCrystal(LCD)ライブラリの初期化コード(2009.05.25)
- ArduinoでNTPを使用する(2009.06.07)

コメント