Makefileで並列処理 / jオプション その2 エラー回避策

並列処理するため”make all -j12″を実行した場合に初回エラーになる不具合の解決策が見つかりました。

エラー内容を確認すると、オブジェクトファイルが揃わないうちにリンクしていることが判明しました。そこでMakefile内の順番を変えて、必ずコンパイルが全て終わってからリンクするようにしました。

これで確実に並列処理できるようになりました。

# コンパイル後リンク ← この部分を前にもってきた
.PHONY: all
all: $(TARGET)

# oファイルから実行ファイルとappファイル作成
$(TARGET): $(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY)
	mkdir -p $(TARGET).app/Contents/MacOS
	mkdir -p $(TARGET).app/Contents/Resources
	cp $(TARGETDIR)/$(TARGET) $(TARGET).app/Contents/MacOS/$(TARGET)
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" > $(TARGET).app/Contents/Info.plist
	echo "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" >> $(TARGET).app/Contents/Info.plist
	echo "<plist version=\"1.0\">" >> $(TARGET).app/Contents/Info.plist
	echo "<dict>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleExecutable</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIconFile</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET).icns</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleIdentifier</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>com.yourcompany.$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleName</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>$(TARGET)</string>" >> $(TARGET).app/Contents/Info.plist
	echo "  <key>CFBundleVersion</key>" >> $(TARGET).app/Contents/Info.plist
	echo "  <string>1.0</string>" >> $(TARGET).app/Contents/Info.plist
	echo "</dict>" >> $(TARGET).app/Contents/Info.plist
	echo "</plist>" >> $(TARGET).app/Contents/Info.plist

# 全ファイル削除
.PHONY: clean
clean:
	rm -rf $(OBJS) $(TARGETDIR)/$(TARGET) $(TARGET).app

Makefileで並列処理 / jオプション その1

CPUのコアをフルに使ってビルドする場合はjオプションを追加します。諸説ありますが、基本的にはコア数を付けてビルドします。

# M2 Pro Mac miniは12コア
make all -j12

Makefileはそのまま使えます。今のところ最初のmakeコマンドでは失敗しますが、2回目で正常にビルドできます。

オブジェクトファイルの作成を並列処理できるので、その工程は大体1/cppファイル数(12以下)の処理時間に短縮できます。今回は8ファイルなので約1/8です。かなりの高速化になります。ただしMakefileを複数用いる場合は何らかの工夫が必要になるようです。

比較的簡単なプログラムであればものの数秒でビルドできてしまいます。ここまで高速化できると、C++を扱うホビープログラマにとってはM1で十分です。

ただXcodeでSwiftのプロジェクトをビルドするとそれなりに時間がかかるため、M2 Proでもまだ足りないですね。

今は1回目に必ずビルド失敗する状況ですから、最初からmake allコマンドでシングルコア・ビルドしていれば良いわけでちょっと微妙な改良ではあります。ビルドに数分かかるような大きめのプログラムでしたら導入の価値ありです。

[C++] 382 SwitchBot管理アプリの製作 その9 UserDefaultsでデータ永続化 Objective-C++, wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

設定データを保存する方法としてCSVを考えていましたが、macOSアプリですからOS固有の方法を活用してみたくなりました。

Objective-C++で書いた関数を使って設定データをUserDefaultsで管理するようにしました。

以前開発していたBBSブラウザと同様、cppファイルとmmファイルがプロジェクトに混在する形となります。

#include <iostream>
#import <Foundation/Foundation.h>

void saveToUserDefaults(const std::string& key, const std::string& value) {
    NSString *nsKey = [NSString stringWithUTF8String:key.c_str()];
    NSString *nsValue = [NSString stringWithUTF8String:value.c_str()];
    [[NSUserDefaults standardUserDefaults] setObject:nsValue forKey:nsKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

std::string getFromUserDefaults(const std::string& key) {
    NSString *nsKey = [NSString stringWithUTF8String:key.c_str()];
    NSString *nsValue = [[NSUserDefaults standardUserDefaults] stringForKey:nsKey];
    if (nsValue) {
        return std::string([nsValue UTF8String]);
    } else {
        return "";
    }
}

[C++] 381 SwitchBot管理アプリの製作 その8 エアコン自動運転 wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

エアコン自動運転機能を実装しました。

エアコン動作温度を0.1度単位で設定し、温度幅を純正アプリの0.5度から最小0.1度にすることができます。

これでエアコン操作をこのアプリに任せて、なおかつ浴室の状態を常にモニタリングできるようになりました。グラフは過去の数値を最大4時間分プロットしています。

次はiPhone版やiPad版をC++(Objective-C++)ベースで作れるかどうか検討します。

[C++] 380 SwitchBot管理アプリの製作 その7 グラフ埋め込み wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

C++にはmatplotlibライブラリに匹敵するグラフ作成ライブラリが見つからず、ラッパーであるmatplotlib-cppは古いためかアプリがクラッシュします。

Py_Initialize()などを使ってPythonスクリプトをモジュール化しましたが、wxWidgetsのonTimer関数ではうまく動作せず落ちてしまうので、スクリプトのままターミナルで実行しました。

これで絶対湿度など数値とグラフをチェックし、進捗を確認できるようになりました。

#include "CallPythonScript.h"
#include <cstdio>
#include <string>
#include <iostream>
#include <fstream>

void CallPythonScript(const std::string& csvPath, const std::string& pngPath) {
    std::string scriptPath = "GenerateGraph.py";
    std::string command = "/usr/local/bin/python " + scriptPath + " " + csvPath + " " + pngPath + " > /dev/null 2>&1";

    FILE* pipe = popen(command.c_str(), "r");
    if (!pipe) {
        std::cerr << "Failed to execute Python script" << std::endl;
        return;
    }

    int result = pclose(pipe);
    if (result != 0) {
        std::cerr << "Failed to execute Python script" << std::endl;
    } else {
        std::ifstream file(pngPath);
        if (!file) {
            std::cerr << "PNG file not found or cannot be opened" << std::endl;
        } else {
            std::cout << "PNG file created successfully" << std::endl;
        }
    }
}

[C++] 379 SwitchBot管理アプリの製作 その6 浴室乾燥チェック実装 wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

洗濯物が乾燥しているであろう絶対湿度まで下がる過程で段階的にアプリの表示が変わるようにしました。

絶対湿度は5分間の移動平均値(右側の数値)で判定しています。

あとは実際の浴室乾燥でテストしながらアプリを調整していきます。

これでガス代を少しは節約できそうです。また浴室の相対湿度を70%以下に近づけるよう意識することでカビの発生を低減できます。

[C++] 378 SwitchBot管理アプリの製作 その5 APIリクエスト wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

エアコンを操作するAPIリクエストのところでつまづきました。

Pythonでは100%成功していましたが、C++では成功率50%以下になりました。色々調べた結果、HMAC_CTXによる署名生成に問題があることが判明し、これを使わずに署名生成して解決しました。

Pythonではシンプルに正常なコードになる場合でも、C++に移植するのは容易ではないことがたまにあります。

#include "signGenerate.h"
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <openssl/buffer.h>
#include <uuid/uuid.h>

std::string generate_uuid() {
    uuid_t uuid;
    uuid_generate_random(uuid);
    char uuid_str[37];
    uuid_unparse(uuid, uuid_str);
    return std::string(uuid_str);
}

std::string base64_encode(const unsigned char* input, int length) {
    BIO *bmem, *b64;
    BUF_MEM *bptr;

    b64 = BIO_new(BIO_f_base64());
    bmem = BIO_new(BIO_s_mem());
    b64 = BIO_push(b64, bmem);

    BIO_write(b64, input, length);
    BIO_flush(b64);
    BIO_get_mem_ptr(b64, &bptr);

    std::string output(bptr->data, bptr->length - 1);
    BIO_free_all(b64);

    return output;
}

std::string hmac_sha256(const std::string& key, const std::string& data) {
    unsigned char* digest;
    digest = HMAC(EVP_sha256(), key.c_str(), key.length(), (unsigned char*)data.c_str(), data.length(), NULL, NULL);

    return std::string((char*)digest, SHA256_DIGEST_LENGTH);
}

[C++] 376 SwitchBot管理アプリの製作 その3 ボタンのカスタマイズ wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

ボタンの角を丸くして枠を消しラベル色と背景色を指定できるRoundedButtonクラスをChatGPTに作成してもらいました。当然ヘッダファイルもさくっと作ってくれます。

FLTKを苦労して学んでいたころからは信じられない楽さです。もっともあのころにC++の基本やMakefileの作り方を習得していたからできることではあります。

#include "RoundedButton.h"

RoundedButton::RoundedButton(wxWindow* parent, wxWindowID id, const wxString& label, const wxPoint& pos, const wxSize& size, const wxColour& bgColor, const wxColour& labelColor)
    : wxButton(parent, id, label, pos, size), m_bgColor(bgColor), m_labelColor(labelColor)
{
    SetBackgroundStyle(wxBG_STYLE_PAINT);
    Bind(wxEVT_PAINT, &RoundedButton::OnPaint, this);
}

void RoundedButton::OnPaint(wxPaintEvent& event)
{
    wxAutoBufferedPaintDC dc(this);
    wxSize size = GetSize();
    wxRect rect(0, 0, size.x, size.y);

    dc.SetBrush(wxBrush(m_bgColor));
    dc.SetPen(*wxTRANSPARENT_PEN);
    dc.DrawRoundedRectangle(rect, 10); // 角の半径を10に設定

    dc.SetTextForeground(m_labelColor); // ラベルの色を設定
    dc.DrawLabel(GetLabel(), rect, wxALIGN_CENTER);
}
// AUTOボタン
new RoundedButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30), wxColour("#00FFFF"), wxColour("#C0C0C0")); // シアン背景、白ラベル

// OFFボタン
new RoundedButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30), wxColour("#FF00FF"), wxColour("#FFFFFF")); // マゼンタ背景、白ラベル

[C++] 375 SwitchBot管理アプリの製作 その2 GUIの作成 wxWidgets

[Mac M2 Pro 12CPU, Sonoma 14.5, wxWidgets 3.2.5]

wxWidgetsのコードはFLTKとはまた違った独特のクセがあります。

ChatGPTにGUIのコードを書いてもらいました。Swiftと遜色ない洗練された外観です。

やはりGUIは座標を使うのが楽です。SwiftUIやJavaのSwingは書きにくくて苦手です。

#include <wx/wx.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/choice.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/image.h>
#include <wx/bitmap.h>
#include "functions.h"

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame(NULL, wxID_ANY, "SwitchBot Manager", wxDefaultPosition, wxSize(600, 400))
    {
        // パネルを左右に分割
        wxPanel* leftPanel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400));
        wxPanel* rightPanel = new wxPanel(this, wxID_ANY, wxPoint(300, 0), wxSize(300, 400));

        // 背景色の設定
        leftPanel->SetBackgroundColour(wxColour("#00CED1"));
        rightPanel->SetBackgroundColour(wxColour("#4B0082"));

        // 下部のパネルを追加して背景色を設定
        wxPanel* bottomPanel = new wxPanel(leftPanel, wxID_ANY, wxPoint(0, 100), wxSize(300, 300));
        bottomPanel->SetBackgroundColour(wxColour("#008B8B"));

        // CSVからのデータ読み込み
        std::vector<double> data = ReadCsvData();

        // 室温と湿度
        new wxStaticText(leftPanel, wxID_ANY, "室温", wxPoint(10, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.1f", data[0]), wxPoint(100, 10), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, "湿度", wxPoint(10, 40), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[1]), wxPoint(100, 40), wxSize(80, 20));

        // 不快指数
        new wxStaticText(leftPanel, wxID_ANY, "不快指数", wxPoint(10, 70), wxSize(80, 20));
        new wxStaticText(leftPanel, wxID_ANY, wxString::Format("%.0f", data[2]), wxPoint(100, 70), wxSize(80, 20));

        // エアコン
        new wxStaticText(bottomPanel, wxID_ANY, "エアコン", wxPoint(10, 10), wxSize(80, 20));

        // AUTOボタン
        new wxButton(bottomPanel, wxID_ANY, "AUTO", wxPoint(100, 5), wxSize(60, 30));

        // OFFボタン
        new wxButton(bottomPanel, wxID_ANY, "OFF", wxPoint(180, 5), wxSize(60, 30));

        // 動作温度
        new wxStaticText(bottomPanel, wxID_ANY, "動作温度", wxPoint(10, 51), wxSize(72, 18));
        wxSlider* operationTempSlider = new wxSlider(bottomPanel, wxID_ANY, 26, 26, 28, wxPoint(110, 50), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* operationTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "26", wxPoint(240, 50), wxSize(40, 20));

        // 温度幅
        new wxStaticText(bottomPanel, wxID_ANY, "温度幅", wxPoint(10, 81), wxSize(54, 18));
        wxSlider* tempRangeSlider = new wxSlider(bottomPanel, wxID_ANY, 3, 1, 5, wxPoint(110, 80), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* tempRangeDisplay = new wxStaticText(bottomPanel, wxID_ANY, "0.3", wxPoint(240, 81), wxSize(40, 20));

        // 設定温度
        new wxStaticText(bottomPanel, wxID_ANY, "設定温度", wxPoint(10, 111), wxSize(72, 18));
        wxSlider* setTempSlider = new wxSlider(bottomPanel, wxID_ANY, 22, 20, 25, wxPoint(110, 110), wxSize(120, 20), wxSL_HORIZONTAL);
        wxStaticText* setTempDisplay = new wxStaticText(bottomPanel, wxID_ANY, "22", wxPoint(240, 110), wxSize(40, 20));

        // ファン
        new wxStaticText(bottomPanel, wxID_ANY, "ファン", wxPoint(10, 141), wxSize(53, 18));
        wxArrayString fanChoices;
        fanChoices.Add("Auto");
        fanChoices.Add("Low");
        fanChoices.Add("Medium");
        fanChoices.Add("High");
        new wxChoice(bottomPanel, wxID_ANY, wxPoint(110, 140), wxSize(80, 20), fanChoices);

        // Event bindings
        operationTempSlider->Bind(wxEVT_SLIDER, [operationTempDisplay](wxCommandEvent& event) {
            operationTempDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        tempRangeSlider->Bind(wxEVT_SLIDER, [tempRangeDisplay](wxCommandEvent& event) {
            tempRangeDisplay->SetLabel(wxString::Format("%.1f", event.GetInt() / 10.0));
        });

        setTempSlider->Bind(wxEVT_SLIDER, [setTempDisplay](wxCommandEvent& event) {
            setTempDisplay->SetLabel(wxString::Format("%d", event.GetInt()));
        });
    }
};

class MyApp : public wxApp
{
public:
    virtual bool OnInit()
    {
        MyFrame* frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);