[C++] 351 BBS閲覧アプリの製作 その32 掲示板クリック時に落ちる / クラッシュレポートの閲覧

[Mac M2 Pro 12CPU, MacOS Ventura 13.6, clang 15.0.0]

昨晩、自製BBS閲覧アプリで特定の掲示板をクリックするとアプリが落ちるようになってしまいました。

クラッシュレポートからXPath式を評価する際にエラーになることが判明しました。エラーになってもアプリが落ちないようにexecuteXpath関数の戻り値をNULL以外に変えるなど対処しましたが、結局うまくいきませんでした。

今朝になって正常動作するようになったものの、再発が懸念されます。クラッシュレポートは後からでもmacOSのコンソールアプリから閲覧できます。一応、エラーが発生しそうな箇所を修正して予防対策しておきました。戻り値云々は見当違いのようです。

当時の他の類似アプリでの挙動は確認できていませんが、ちょっとしたトラブルでもアプリが絶対に落ちないよう堅牢性を持たせたいものです。

// XPath式で要素を取得
xmlNodeSetPtr executeXpath(xmlDocPtr &doc, xmlChar *xpath_expr) {
    xmlXPathContextPtr xpath_context;
    xmlXPathObjectPtr  xpath_obj;

    xpath_context = xmlXPathNewContext(doc);
    if (xpath_context == NULL) {
        cerr << "Error: unable to create new XPath context" << endl;
        xmlFreeDoc(doc);
        return NULL;
    }
    xmlNodePtr node = xmlDocGetRootElement(doc);

    // XPath式を評価
    // 23/10/16 エラー発生, 23/10/17 回復
    xpath_obj = xmlXPathEvalExpression(xpath_expr, xpath_context);
    if (xmlXPathNodeSetIsEmpty(xpath_obj->nodesetval)) {
        cerr << "Error: unable to evaluate xpath expression" << endl;
        xmlXPathFreeContext(xpath_context);
        xmlFreeDoc(doc);
        return NULL;
    }

    return xpath_obj->nodesetval;
}
    xmlDoc* doc = htmlReadMemory(htmlBuffer.c_str(), htmlBuffer.size(), nullptr, nullptr, HTML_PARSE_RECOVER);
    if (doc) {
        xmlNodeSetPtr tradObj = executeXpath(doc, (xmlChar *)"//*[name()='small']/*[name()='a']");

        if (tradObj) {
            for (int i = 0; i < tradObj->nodeNr; i++) {
                xmlNodePtr node = tradObj->nodeTab[i];
                xmlChar* id0 = xmlGetProp(node, (xmlChar*)"href");
                xmlChar* title0 = xmlNodeGetContent(node);

                string id = convertToString(id0);
                id.erase(id.size() - 4);
                string title = convertToString(title0);
                // cout << "title: " << title << endl;

                idTitle.push_back(std::make_pair(id, title));
                
                xmlFree(id0);
                xmlFree(title0);
            }
        } else {
            return {}; // else部分がなかったので追記 23/10/17
        }
        xmlFreeDoc(doc);
    } else {
        return {}; // else部分がなかったので追記 23/10/17
    }
クラッシュレポートは後からでもコンソールアプリで確認可能
(保存期間は不明)

[C++] 350 ChatGPTアプリの製作 その37 libcrypto.libが見つからず起動しない

[Mac M2 Pro 12CPU, MacOS Ventura 13.6, clang 14.0.3]

今朝、自製ChatGPTアプリを起動しようとすると途中で落ちてしまうようになりました。

Homebrewのopensslライブラリを更新すると直りました。エラーレポートにあるlibcrypto.libは暗号化に関連するlibファイルです。

余談ですが、新macOS Sonomaは2023/10/11現在まだ初期バージョン14.0のままなので次のバージョンになったらサブ機に入れてみる予定です。

brew update & brew upgrade & brew install openssl
自製ChatGPTアプリの最新UI

参考サイト
Apple Developer Forums

[C++] 349 FLTK : ChatGPTアプリの製作 その36 jsonファイルをフォルダに振り分ける

[Mac M2 Pro 12CPU, MacOS Ventura 13.5, clang 14.0.3]

自製ChatGPTアプリに関する記事は3月以来5ヶ月ぶりです。記事にはしていませんが、これまで何度も更新を重ねグレードアップしています。

ChatGPTアプリでのやりとりはjsonファイルとして保存しています。大分ファイルがたまってきたため、年月フォルダ(今月の場合は”2308″)を自動作成して振り分けるMovJSボタンを実装しました。

ChatGPTにコードを書いてもらいました。一応コードはチェックして問題なさそうなのでビルド&実行したところ一発で成功しました。

実装完了までものの10分です。AIアシストでプログラミングすると生産性爆上がりです。

#include <iostream>
#include <filesystem>
#include <fstream>

namespace fs = std::filesystem;

void movJSONCB(Fl_Widget*, void*){
    fs::path srcDir = "/ChatGPT";

    // srcDirにあるjsonファイルのpathをリスト化する
    std::vector<fs::path> jsonFiles;
    for (const auto& entry : fs::directory_iterator(srcDir)) {
        if (entry.path().extension() == ".json") {
            jsonFiles.push_back(entry.path());
        }
    }
    
    // ファイル名の先頭4文字のフォルダを作成する
    for (const auto& jsonFile : jsonFiles) {
        std::string folderName = jsonFile.filename().stem().string().substr(0, 4);
        fs::path folderPath = srcDir / folderName;
        
        // フォルダがすでに存在する場合は上書き作成しない
        if (!fs::exists(folderPath)) {
            fs::create_directory(folderPath);
        }
        
        // ファイルをそれぞれのフォルダに振り分ける
        fs::path destinationPath = folderPath / jsonFile.filename();
        fs::rename(jsonFile, destinationPath);
    }
}

[C++] 348 size_tの大きさ

[Mac M2 Pro 12CPU, MacOS Ventura 13.3.1, clang 14.0.3]

size_tはサイズを表す符号なし整数型データです。

符号なしの整数型であればunsigned intでいいのではないかと考えることもできますが、size_tはポインタを格納する場合もあるため4バイトのunsigned intでは足りないです。8バイト必要になります(64ビットプラットフォームの場合)。

コードで出力しようとするとintと同じように扱えるので何のために存在しているのか疑問に思っていましたが、ようやく理解できました。

[C++] 347 BBS閲覧アプリの製作 その31 無効なシーケンス混入による文字コード変換失敗 iconv / CP932

[Mac M2 Pro 12CPU, MacOS Ventura 13.3.1, clang 14.0.3]
(今回からM2 Pro 12コアCPUを使用)

とあるdatファイルをCP932からUTF-8へ変換しようとしたところエラーになりました。

どうやら無効なシーケンスが混ざっているようです。

以下のようにConvertShiftJISToUTF8関数を修正しました。

string ConvertShiftJISToUTF8(const string& input) {
    string output;

    // iconv_t cd = iconv_open("UTF-8", "SHIFT_JISX0213");
    iconv_t cd = iconv_open("UTF-8", "CP932");
    if (cd == (iconv_t)-1) {
        std::cerr << "Error: Failed to open iconv" << std::endl;
        return output;
    }

    size_t inBytes = input.size();
    size_t outBytes = inBytes * 4;
    char* inBuf = const_cast<char*>(input.c_str());
    char* outBuf = new char[outBytes];
    char* outPtr = outBuf;

    while (iconv(cd, &inBuf, &inBytes, &outPtr, &outBytes) == (size_t)-1) {
        if (errno == EILSEQ) {
            std::cerr << "EILSEQ:入力バッファに無効なシーケンスが含まれている" << std::endl;
            inBuf += 1; // 無効なシーケンスの分だけ入力バッファを進める
            inBytes -= 1; // 無効なシーケンスの分だけ入力バッファのサイズを減らす

        } else if (errno == EINVAL) {
            std::cerr << "EINVAL:入力バッファの終端が不完全" << std::endl;
            delete[] outBuf;
            iconv_close(cd);

            return "";
        } else if (errno == E2BIG) {
            std::cerr << "E2BIG:出力バッファが不足している" << std::endl;
            delete[] outBuf;
            iconv_close(cd);

            return "";
        } else {
            std::cerr << "その他のエラー" << std::endl;
            delete[] outBuf;
            iconv_close(cd);

            return "";
        }
    }
    output.assign(outBuf, outPtr - outBuf);

    delete[] outBuf;
    iconv_close(cd);

    return output;
}

ChatGPTに有益なヒントをもらいましたが、例示するコードが実にいい加減で少し振り回されました。追及しても虚偽説明が積み上がっていくばかりで無意味なやりとりになります。ChatGPTはネット情報から引用するだけで、検証する能力は皆無ですね。まあそんなもんです。

datファイルがCP932ではなく他のシフトJISを採用している可能性も考えましたが、今回は当てはまりませんでした。

ちなみにCP932(Windows-31J)と他のシフトJISとの文字集合の関係は以下のオイラー図のようになります。CP932は黄色です。

User:Hissakun~commonswiki, CC 表示-継承 3.0, https://commons.wikimedia.org/w/index.php?curid=82138426による

[C++] 346 BBS閲覧アプリの製作 その30 レス番号へスクロール JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

レスアンカー先へクリックでスクロールできるようにしました。

意外と手間取りました。HTML埋め込みのJavaScriptではtopプロパティで絶対Y座標が取得できるのですが、JavaScriptファイルでは相対Y座標でした。

なのでwindow.scrollYで現在位置を把握し、これにアンカー先の相対Y座標を加算する形になります。

// クリックしたレス番号までスクロール
function moveToRes(element){
    var resNum = element.innerText.replace(/>>/g, '');
    console.log("resNum: " + resNum);

    var targetElement = document.getElementById(resNum);
    var rect = targetElement.getBoundingClientRect();
    var yTo = rect.top;
    console.log("yTo = " + yTo);

    var currentY = window.scrollY;
    var scrollToY = currentY + yTo;
    console.log("scrollToY = " + scrollToY);

    scrollTo(0, scrollToY);
}

[C++] 345 BBS閲覧アプリの製作 その29 動画サイトへのリンク JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

投稿文字列から動画URLを検知してリンクをはり、クリックで再生できるようにしました。

現状GUI右下のFl_WebView縦横をフルに使っての再生です。本当は小さくポップアップさせたいのですが、上手くできません。JQueryを使えばできるようになるのでしょうか。

あとマウスアウトで画面消去したいところで、ホバー対象が画面に隠れてしまっていてできません。スレッドをリロードするしかないです。

クリックすると即再生は危険なので、まずマウスオーバーで画像だけポップアップさせたいですね。

2023/8/1追記
その後の検証でJavascriptのshowVideo関数は動作しておらず、Fl_WebViewの動画再生機能で表示されていることが判明しました。つまりhrefリンクするだけで再生できるようになります。

// 動画再生
function showVideo(element,event) {
    // event.preventDefault();

    var videoSrc = element.getAttribute("href");
    var videoElement = document.createElement("video");
    videoElement.src = videoSrc;
    videoElement.classList.add("popup-video");
    document.body.appendChild(videoElement);
    videoElement.style.display = "block";
    // videoElement.play();

    window.addEventListener("resize", resizeVideo); // 機能せず

    var rect = videoElement.getBoundingClientRect();
    var y = rect.top;
    console.log("Popup y = " + y);

    videoElement.style.top = -y + 50;
}

// 動画消去
function eraseVideo() {
    var videoElement = document.querySelector(".popup-video");
    if (videoElement) {
        videoElement.pause();
        videoElement.parentNode.removeChild(videoElement);
    }

    window.removeEventListener("resize", resizeVideo); // 機能せず
}
// datファイルから各行を取り出し投稿内容msg2を抽出してからの処理

// mp4リンク化
size_t posMP = msg2.find("https");
while (posMP != string::npos) {
      string urlMP4;
      size_t posMP2 = msg2.find(".mp4", posMP);
      if (posMP2 != string::npos){
            urlMP4 = msg2.substr(posMP, posMP2 - posMP + 4);
      } else {
            break;
      }

      string aTagMP4 = "<a href=\"" + urlMP4 + "\" rel=\"noopener noreferrer\" onclick=\"showVideo(this,event);\" onmouseout=\"eraseVideo();\" >" + urlMP4 + "</a>";

      cout << "aTagMP4: " << aTagMP4 << endl;

      msg2.erase(posMP, posMP2 - posMP + 4);
      msg2.insert(posMP, aTagMP4);

      posMP = msg2.find("https", posMP + aTagMP4.size());
}        

[C++] 344 C++とObjective-C混在プロジェクトのMakefile

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

GitHubにあるFl_WebViewをより深く理解するため、ライブラリではなくソースファイル(cpp1個、m1個)としてプロジェクトに取り込み、ビルドしてみました。

プロジェクトのソースファイルはcppファイル14個、mファイル1個で構成されています。

Makefileは以下の通りです。ビルドは成功し、アプリは正常に動作しています。

# コンパイラ
COMPILER = clang++
DEBUG = -g

# オプション
CPPFLAGS = -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_THREAD_SAFE -D_REENTRANT -std=c++20
MFLAGS = -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_THREAD_SAFE -D_REENTRANT
LDFLAGS = -lfltk -lpthread -framework Cocoa -framework WebKit -lc++

# includeパス(-I)
INCLUDE = -I./include -I/Volumes/DATA_m1/code/cpp/mylib/include -I/usr/local/include \
-I/opt/homebrew/Cellar/libxml2/2.10.3_2/include

# ライブラリ(-l)
LIBRARY0 = -lcurl -liconv -lxml2

# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib

# ソースファイル
SRCDIR = ./src
SRCS_CPP = $(shell find $(SRCDIR) -type f -name "*.cpp")
SRCS_M = $(shell find $(SRCDIR) -type f -name "*.m")

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS_CPP))) \
$(addprefix $(OBJDIR), $(patsubst ./src/%.m,/%.o,$(SRCS_M)))

# 実行ファイル
TARGETDIR = ./bin
TARGET = BBS_Browser

# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(COMPILER) $(CPPFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# mファイルからoファイル作成
$(OBJDIR)/%.o: $(SRCDIR)/%.m
	$(COMPILER) $(MFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# アプリファイル作成関連
POSTBUILD  = fltk-config --post

# oファイルから実行ファイルとappファイル作成
$(TARGET):$(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY) 
	cp $(TARGETDIR)/$(TARGET) $(TARGET)
	$(POSTBUILD) $(TARGET)
	mkdir $(TARGET).app/Contents/Resources
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	plutil -insert 'CFBundleIconFile' -string $(TARGET).icns $(TARGET).app/Contents/Info.plist
	rm -f $(TARGET)

# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)

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

[C++] 343 GUI上の座標を自動クリック macOSのApplication Services

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

BBS閲覧アプリで右上のスレッドタイトルをクリックして、右下のスレッド欄をホバーしてもJavaScriptが機能せず、Fl_WebView上を1回クリックする必要があります。

このクリックを自動でさせるコードを書きました。macOSのApplication ServicesというAPIを使います。

これはCarbon APIに分類され、Mac OS(今はmacOS、その前はMac OS X、さらに前がMac OS)用のフレームワークをmacOSに移植したものです。

Objective-Cで書かれているために汎用性が低いCocoa APIとは異なり、C言語やC++で書かれているのでライブラリとしてそのまま使えます。

今回はFl_WebViewへの自動クリックに使いましたが、スレッド選択領域に戻る際にクリックが必要になるという副作用が生じたため採用は見送りました。

Carbon APIについては、いずれ切り捨てられてしまう可能性があるものの、それまでは活用する価値はあると思います。

#include <ApplicationServices/ApplicationServices.h>

Fl_Window* win; // 親ウィジェット
Fl_Window* win2; // 子ウィジェット

// 親ウィジェットは絶対座標
int x = win ->x();
int y = win ->y();
cout << "win x座標: " << x << " y座標: " << y << endl;

// 子ウィジェットは相対座標
int x2 = win2 ->x();
int y2 = win2 ->y();
cout << "win2 x座標: " << x_win << " y座標: " << y_win << endl;

// クリックイベントを作成
CGEventRef clickEvent = CGEventCreateMouseEvent(
    NULL, kCGEventLeftMouseDown,
    CGPointMake(x + x2 +5, y + y2 +5),
    kCGMouseButtonLeft
);

// イベントを送信
CGEventPost(kCGHIDEventTap, clickEvent);

// イベントを解放
CFRelease(clickEvent);

[C++] 342 BBS閲覧アプリの製作 その28 スレッドタイトル検索機能実装

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

これは簡単に実装できました。15分位でしょうか。

これで基本的な機能は一通り備わったように思います。ボードによってはまれにスレッドタイトル読込で落ちてしまったりしますが、ぼちぼち直していきます。

ソースファイルが13個になり分かりにくくなってきたので、複数フォルダへの振り分けを検討しています。

高度な検索にかけたり、過去ログも含めて管理するのでしたらMySQLなどデータベースの出番もあるのでしょうが、今の用途であれば都度2次元ベクター化で事足りますね。

#include "cppstd.h" // 自製ライブラリ
#include "MyTable.h"

extern vector<tuple<string, string, string, string>> numTitlePostnumID;
extern Fl_Input* searchInput;
extern MyTable* table;

void threadSearch(Fl_Widget *w, void*){
    vector<tuple<string, string, string, string>> searchResult;
    const char* word = searchInput -> value();

    for (const auto& tuple : numTitlePostnumID) {
        string title = std::get<1>(tuple);
        if (title.find(word) != string::npos) {
            searchResult.push_back(make_tuple(std::get<0>(tuple), std::get<1>(tuple), std::get<2>(tuple), std::get<3>(tuple)));
        }
    }

    int hitNum = searchResult.size();
    numTitlePostnumID = searchResult;

    table->SetSize(hitNum, 5);
}