ぷるぷるの雑記

低レイヤーがんばるぞいなブログ. 記事のご利用は自己責任で.

Arduinoで上下左右ボタンを押すと固まる

Arduino Uno R4 Minima で以下のようにシリアルからの入力をエコーバックするプログラムを書きました. ポチポチポチと文字を送信する分には正常に動きますが、シリアルコンソールの上下左右ボタンを押すと以降どのボタンを押しても反応が返ってきません

void setup() {
  Serial.begin( 9600 );
  Serial.println( "Hello Arduino!" );

void loop() {
  char key;
  do{
    if ( Serial.available() == true ) {
      key = Serial.read();
      Serial.write( key );
    }
  }while(1);
}

これはSerial.available() シリアルバッファに貯まっているデータのデータサイズを返すメソッド だからです.

解説

まず、PCのシリアルコンソール(Teratermを想定)で矢印ボタンを押すとはエスケープシーケンス用のデータを送信します. 具体的には以下の表のような3Byte(3文字)のデータが送信されます.

ボタン 送信データ(BYTE) 送信データ(文字)
0x1b 0x5b 0x41 '\e' '[' 'A'
0x1b 0x5b 0x42 \e' '[' 'B'
0x1b 0x5b 0x43 \e' '[' 'C'
0x1b 0x5b 0x44 \e' '[' 'D'

この3Byteはほぼ同時に送信されるため、タイミングにもよりますがArduinoのシリアルの受信バッファに3Byteのデータが格納されます. この状況でSerial.available()を呼び出すと3が返ってくるためtrueと一致せず無限ループに陥ってしまったというわけです.

感想

メソッドの名前が良くない.

参考

https://docs.arduino.cc/language-reference/en/functions/communication/serial/available/

CreateCompatibleDCとCreateCompatibleBitmapの使い方

WinAPIのCreateCompatibleDCとCreateCompatibleBitmapの自分用のメモです.

バイスコンテキストとは

バイスコンテキストといったときのデバイスはたいてい以下のどれかに分類されます.

種類 説明 主な作成方法
ディスプレイデバイスコンテキスト いわゆるウインドウ CreateWindow関数 or CWndクラスのインスタンス作成
プリンターデバイスコンテキスト プリンター CreateDC関数
メモリデバイスコンテキスト ダブルバッファリングなどで使用するメモリ上の疑似デバイス CreateCompatibleDC関数


ディスプレイやプリンターは別のデバイスですが、絵や文字などのピクセル情報を出力するという点で同じものです. なので両者を同一のインターフェースで扱うための仕組みがデバイスコンテキストです. ダブルバッファリングや画像の加工もできるようにメモリ上の領域を疑似デバイスとして扱うためのメモリデバイスコンテキストという仕組みも存在します.


バイスコンテキストという仕組みを使うと異なるデバイスを同一のインターフェースで扱えるようになることに加え、TextOut関数やRectangle関数などのWinAPIに備わる初等的な描画関数を利用することが出来るようになります.

バイスコンテキストの互換性とBitBlt関数

バイスコンテキストに関わるAPIとして BitBlt関数 があります. この関数を使うとソースデバイスコンテキストのピクセルをターゲットデバイスコンテキストへ ブロック転送(高速コピー) することが出来ます. ただし、 BitBltで指定するターゲットデバイスコンテキストはソースデバイスコンテキストと互換性がある必要があります. この互換性があるというのはどういうことでしょうか.

先ほど異なるデバイスを統一的に扱う仕組みがデバイスコンテキストだと述べましたが、各種コンテキストに特有の設定がどうしても残ります. デバイスコンテキストの設定に関するものとしてDEVMODEA構造体とGetDeviceCaps関数に触れておきましょう. Microsoftのリファレンスを読むと、DEVMODEA構造体はプリンターまたは表示装置の設定に関する構造体で、GetDeviceCaps関数はデバイス固有の情報を取得するための関数とありますが、その項目が膨大にあることが分かります. つまり、バイスコンテキストの設定は膨大にあります.

learn.microsoft.com

learn.microsoft.com


そこで活躍するのが CreateCompatibleDC関数 というわけです. この関数を使うと細かい設定を気にせずとも引数で指定したデバイスコンテキストと互換性がある、つまり BitBlt関数を使用することが出来るメモリデバイスコンテキスト を作成することが出来ます.

www.net3-tv.net

CreateCompatibleDC関数だけではBitBlt関数は使用できない

実際にBitBlt関数を使用するにはCreateCompatibleDC関数の後にメモリ領域の確保が必要になります. CreateCompatibleDC関数はあくまで互換性のあるメモリデバイスコンテキストを作成するだけで、 ピクセルデータを保持しておくメモリ領域 が確保されていません(正確に言うとモノクロ1px分しか確保されていない). これは例えるならばバイスの設定だけを作成してコピー用紙やディスプレイのサイズはコピーしていない 状態です. 一見意地悪な仕様のように見えますが、BitBlt先のデバイスのサイズは必ずしもBitBlt元のデバイスのサイズとは限らないため、無駄にメモリを確保しないための仕様らしいです.

CreateCompatibleDC関数で作成したメモリデバイスコンテキストのメモリ領域を確保するためには ビットマップオブジェクトをSelectObject関数で指定する必要があります. ビットマップオブジェクトには色情報(モノクロなのかグレスケなのかRGBなのかなど)とサイズ情報が含まれているため、最終的に何バイト確保すればよいのか確定します. このビットマップの情報を互換性を保ったままデバイスコンテキストから抽出するための関数が CreateCompatibleBitmap関数 というわけです.

learn.microsoft.com

CreateCompatibleBitmap関数

CreateCompatibleBitmap関数は以下のようなインターフェースで宣言されています.

HBITMAP CreateCompatibleBitmap(
  [in] HDC hdc,
  [in] int cx,
  [in] int cy
);


宣言からも分かるように、サイズ情報はユーザーが与えてやる必要があります. おそらくCreateCompatibleDC関数でメモリ確保は行わないのと同じ理由でしょう. 利用したいデバイスがウインドウハンドルに紐づいたディスプレイ装置の場合はウインドウのクライアント領域のサイズやstaticコンポーネントのサイズなどになるでしょう. サイズを機械的に決定したい場合は過去の記事にもあるようにGetCurrentObject関数を使って複製元のデバイスコンテキストに紐づいたビットマップのサイズを取得するとよいでしょう. prupru-prune.hatenablog.com

CreateCompatibleBitmap関数とLoadBitmap関数の違い

メモリデバイスコンテキストにSelectObjectするビットマップオブジェクトの作成関数は、CreateCompatibleBitmap関数だけではなくLoadBitmap関数でも良いようです. LoadBitmap関数を用いた場合デバイスコンテキストと互換性のあるビットマップになっているのかは不明ですが、とにかくSelectObject関数は成功するようです.

おそらくですが、SelectObject関数自体はビットマップの互換性に関係なくビットマップオブジェクトを指定することが出来、自動的に第一引数で指定したデバイスコンテキストと互換性を持つビットマップとしてアタッチするのではないでしょうか...

具体的な使い方

先人たちを参照されたし

<CreateCompatibleBitmap関数を使う例> wisdom.sakura.ne.jp

<LoadBitmap関数を使う例> programming.pc-note.net

まとめ

  • バイスコンテキストという仕組みを使うことで異なるデバイスを同一のインターフェースで操作することが出来る
  • BitBlt関数を使うためには互換性があるデバイスコンテキストを作る必要がある
  • CreateCompatibleDC関数で作成直後のメモリデバイスコンテキストはメモリが確保されていない
  • メモリデバイスコンテキストのメモリを確保するためにCreateCompatibleBitmap関数を呼んで色やサイズの情報を確定する
  • ただしCreateCompatibleBitmap関数はサイズを指定する必要があるので必要に応じてGetObject関数などを使いビットマップのサイズを取得する
  • SelectObject関数でビットマップオブジェクトを指定するとメモリデバイスコンテキストの領域が確保される
  • SelectObject関数で指定するビットマップオブジェクトはLoadBitmap関数によって作られたものでも良い. SelectObject関数は互換性の有無を吸収する?

参考

learn.microsoft.com

learn.microsoft.com

ja.wikipedia.org

learn.microsoft.com

mikanmarusan.github.io

learn.microsoft.com

learn.microsoft.com

メモリデバイスコンテキストのサイズを取得する

Windowsでメモリデバイスコンテキストのサイズを取得するには以下のようにメモリデバイスコンテキストのビットマップを取得してからビットマップヘッダに情報をコピーします.

WinAPIバージョン

// hDCは対象のウィンドウのデバイスコンテキストとする
HDC hMem = CreateCompatibleDC(hDC);

BITMAP structBitmapHeader;
memset(&structBitmapHeader, 0, sizeof(BITMAP));

HGDIOBJ hBitmap = GetCurrentObject(hMem, OBJ_BITMAP);
GetObject(hBitmap, sizeof(BITMAP), &structBitmapHeader);

// structBitmapHeader.bmWidth
// structBitmapHeader.bmHeight

DeleteObject(hMem);
DeleteObject(hBitmap);


MFCバージョン(ただしWinAPIも必要)

CDC *pDC = this->GetDC();

CDC hDC;
hDC.CreateCompatibleDC(pDC);

CBitmap *pBmp = hDC.GetCurrentBitmap();

BITMAP structBitmapHeader;
memset(&structBitmapHeader, 0, sizeof(BITMAP));

pBmp->GetBitmap(&structBitmapHeader);
// structBitmapHeader.bmWidth
// structBitmapHeader.bmHeight
    
// これだと{0,0}が返ってくるだけ
// CSize sz = pBmp->GetBitmapDimension();

delete pBmp;

この場合CreateCompatibleDC直後なので(1,1)が取得できます.

参考

stackoverflow.com

learn.microsoft.com

learn.microsoft.com

learn.microsoft.com

learn.microsoft.com

learn.microsoft.com

C++のオブジェクト指向でのメモリリークを理解する

C++オブジェクト指向プログラミングでメモリリークをおこしてみましょう.

以下のクラスを考えます. コンストラクタでヒープを確保し、デストラクタで解放するだけのクラスです.

class MyObj
{
public:
    MyObj(size_t byte) :pMem(NULL) {
        uint8_t *ptr = (uint8_t*)malloc(byte);
        if (ptr != NULL) {
            pMem = ptr;
        }
        printf("Constructed\n");
    }
    ~MyObj() {
        free(pMem);
        pMem = NULL;
        printf("Destructed\n");
    }
private:
    uint8_t *pMem;
};


このクラスを次の4パターンで使ってみます.

  1. スタックに確保する(MyObj型のローカル変数として定義する)
  2. ヒープに確保しdeleteしない(newするがdeleteしない)
  3. ヒープに確保しdeleteする(newしてdeleteする)
  4. ヒープに確保しfreeする(newするがdeleteしない)

コードは次のようにしました.

#include<iostream>
#include<windows.h>
#include<stdio.h>
#include<stdint.h>

class MyObj
{
public:
    MyObj(size_t byte) :pMem(NULL) {
        uint8_t *ptr = (uint8_t*)malloc(byte);
        if (ptr != NULL) {
            pMem = ptr;
        }
        printf("Constructed\n");
    }
    ~MyObj() {
        free(pMem);
        pMem = NULL;
        printf("Destructed\n");
    }
private:
    uint8_t *pMem;
};

void func()
{
    /* パターン1
   MyObj obj(1024 * 1024);
   */
    /* パターン2
   MyObj *pObj = new MyObj(1024*1024);
   /* パターン3
   MyObj *pObj = new MyObj(1024*1024);
   delete pObj;
   */
    /* パターン4
   MyObj *pObj = new MyObj(1024*1024);
   free(pObj);
   */
}

int main()
{
    for (int i = 0; i < 1000; i++) {
        func();
    }
    Sleep(INFINITE);
}



各パターンのメモリ使用量を調べましょう. 今回はWindows環境だったのでVisual Studioの診断ツールからデバッグ中のアプリケーションのメモリ使用量を見ました.

パターン1,3のメモリ使用量

パターン2,4のメモリ使用量


以上からメモリリークするのはnewしたもののdeleteを呼んでいないパターン2,4となりました. C言語由来のfree()ではデストラクタは呼ばれないらしいです. 同様にmalloc()ではコンストラクタが呼ばれないようですがクラスのインスタンスmallocで作ることはあまりないように思うので今回の検証からは省きました. つまり、new演算子はヒープ確保+コンストラクタを呼び出す演算子で、delete演算子はヒープ解放+デストラクタを呼び出す演算子ということですね.


以下まとめです.

ただし前者であっても作りによってはメモリリークやハンドルリークの可能性が0ではないので注意.

C#のフォームクラスはファイルの先頭に書かなくてはいけない

C#(というより.NET Framework)のフォームクラスはその.csファイルの最初のクラスでなくてはいけないというルールがあります. このルールが守られていない場合、フォームデザイナーを開くと「クラスFormXXはデザイン出来ますが、ファイルの最初のクラスではありません。... デザイナーを再度読み込んでください。」というエラーが出ます。エラーの内容は.NET FrameworkVisual Studioのバージョンによって多少変わるかもしれません.

具体的には以下のソースコードではフォームデザイナーでエラーが出ますがビルドは出来ます.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp
{
    // このファイルの最初のクラス
    public partial class MyClass
    {
    }

    // このファイルの2番目のクラス
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}


次のソースコード名前空間こそ最初ではないですがクラスは最初のクラスなのでエラーが出ません. ただしたまにデザイナーにより生成されたコードがバグるのでやめた方が良いです.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MySpace
{
}

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}


結局のところフォームクラスは最初の名前空間の最初のクラスとして定義するのがベストっぽいです.

Visual Studioのメモリウィンドウの使い方

検証環境

項目 説明
OS Windows11
Visual Studio 17.8.0 (2022)
言語 C++(MSVC)

開き方

アプリケーションをデバッガで実行中にデバッグ->ウィンドウ->メモリ から開くことが出来ます. メモリ1(1)からメモリ4(4)があり、最大で4つまで同時に開くことが出来ます.

デバッグ->ウィンドウ->メモリからメモリウィンドウを開く

メモリウィンドウの特徴

以下特徴の箇条書き

  • メモリウィンドウ上部にあるテキストボックスにアドレスを入力するとそのアドレスにジャンプ
  • 右上にある列コンボボックスがでメモリダンプの1行当たりの列数を指定できる
  • メモリの値を1バイトずつ上書きすることが出来る
  • 右クリックすると文字コードやデータ型を変更できる

メモリウィンドウの特徴


このアドレスにはアドレスをのものだけではなく、ブレーク中に見えているポインタ変数や&変数の形で変数に入っているアドレスや変数自体のアドレスを指定することも可能です. そしてなんとレジスタを指定することさえできます.

アドレスはポインタ変数でも指定できる

アドレスは&変数名でも指定できる

アドレスはレジスタでも指定できる

参考

learn.microsoft.com

関数内のstatic変数を複数の関数で共有する

C言語で関数内のstatic変数を複数の関数で共有したいときは、関数ポインタを使おうという提案です.

以下のようにstatic変数を定義した関数func()に、実際にstatic変数を使いたい関数ポインタを渡し、func()にstatic変数を引数にセットしてもらおうという算段です.

static変数を実際に使用する関数のインターフェースが一致するとは限らないので、引数の型をvoid * 型にしてお茶を濁しましょう.

#include<stdio.h>

void func(void (*callee)(void *, void *));
void func1(void *para1, void *para2);
void func2(void *para1, void *para2);


int main()
{
    func(func1);
    func(func2);
    func(func1);

    return 0;
}

void func(void (*callee)(void *para1, void *para2)) {
    static int val;
    callee((void*)val++, (void*)3);
}

/* static変数をfuncに注入してもらう */
void func1(void *para1, void *para2) {
    
    int shareVal = (int)para1;
    int cnt = (int)para2;

    for (int i = 0; i < cnt; i++) {
        printf("func1 shareVal=%d\n", shareVal);
    }
}

/* static変数をfuncに注入してもらう */
/* func1とインターフェースを合わせるために引数を2つ受け取るが、実際に使用するのは1つ目だけ */
void func2(void *para1, void *para2) {
    int shareVal = (int)para1;
    printf("func2 shareVal=%d\n", shareVal);
}

/* 実行結果 */
func1 shareVal=0
func1 shareVal=0
func1 shareVal=0
func2 shareVal=1
func1 shareVal=2
func1 shareVal=2
func1 shareVal=2


static変数のアドレスを渡すようにすれば値の参照だけではなく更新もすることが出来ます.

#include<stdio.h>

void func(void (*callee)(void *, void *));
void func1(void *para1, void *para2);
void func2(void *para1, void *para2);


int main()
{
    func(func1);
    func(func2);
    func(func1);

    return 0;
}

void func(void (*callee)(void *para1, void *para2)) {
    static int val;
    callee((void*)(&val), (void*)3);
}

/* static変数の参照だけ */
void func1(void *para1, void *para2) {
    
    const int *pShareVal = (int*)para1;
    int cnt = (int)para2;

    for (int i = 0; i < cnt; i++) {
        printf("func1 shareVal=%d\n", *pShareVal);
    }
}

/* static変数を変更する */
void func2(void *para1, void *para2) {
    int *pShareVal = (int*)para1;
    printf("func2 shareVal=%d\n", *pShareVal);
    *pShareVal = 10;
}

/* 実行結果 */
func1 shareVal=0
func1 shareVal=0
func1 shareVal=0
func2 shareVal=0
func1 shareVal=10
func1 shareVal=10
func1 shareVal=10