C++で libcurlと yajlを使って HTTP(S)で JSON APIを呼び出す

ありふれた処理はどこにでも入れられそうな小さくてありふれたライブラリでやろう

HTTPSでどこかのURLにアクセスして[JSON](https://ja.wikipedia.org/wiki/JavaScript_Object_Notation)で情報をもらってくるというのはよくある処理だが、標準ライブラリにそういうことをするための道具が勢ぞろいしている[Python](https://ja.wikipedia.org/wiki/Python)と違ってそれを C/C++で書こうとするとまずライブラリの選定からしないといけない。また、それだけのために [Boost](https://www.boost.org/) や [Poco](https://pocoproject.org/) といった全部入りライブラリに依存するのはどうにも経済的とは言い難い。 というわけで今回登場するメンバーはこちら - [libcurl](https://curl.se/libcurl/) - 大体どこにでも入れられそうな HTTPクライアントライブラリ。RPMパッケージで 200KBくらい - [yajl](https://lloyd.github.io/yajl/) - 大体どこにでも入れられそうな JSONパーサーライブラリ[^1]。RPMパッケージで 40KBくらい yajlのAPIには[XML](https://ja.wikipedia.org/wiki/Extensible_Markup_Language)でいう[SAX](https://ja.wikipedia.org/wiki/Simple_API_for_XML)のようなストリーム方式([parse](https://lloyd.github.io/yajl/yajl-2.1.0/yajl__parse_8h.html))と[DOM](https://ja.wikipedia.org/wiki/Document_Object_Model)のような一括方式([tree](https://lloyd.github.io/yajl/yajl-2.1.0/yajl__tree_8h.html))があるが、巨大データを扱うので無い限りtreeのほうを使用するのが簡単だ。逆にJSONを生成するには[gen](https://lloyd.github.io/yajl/yajl-2.1.0/yajl__gen_8h.html)を使用する。 今回のサンプルコードを作成するにあたり、[UUID](https://ja.wikipedia.org/wiki/UUID)を生成して返してくれる公開APIがあったのでそれを題材にした。 ``` $ curl https://www.uuidtools.com/api/generate/v1 ["69fcf390-19fe-11ec-884c-a74746582234"] ``` HTTPレスポンスを一旦メモリに全部読み込んでからパースするという処理方法をとる場合、いちおう読み込みサイズに上限を設けておかないともしサーバーがトチ狂って1TBのJSONを送ってきた場合にはメモリを食いつぶしてクラッシュしてしまうことになるので、適当なサイズで制限をするためのチェック処理も挟んである。必要に応じて制限値を緩和すること。 ```c++ #include <memory> #include <iostream> #include <curl/curl.h> #include <yajl/yajl_tree.h> // callback function to receive content fragment static size_t curl_callback(char *buffer, size_t size, size_t nmemb, void *f) { // CURLOPT_MAXFILESIZE has no effect when content length is not known prior to download. // so you need to limit size by your own here. const static size_t limit = 16 * 1024; // Limit size to 16KB std::string& buf = *((std::string*)f); if (buf.length() + size * nmemb > limit) return 0; // tell curl to stop download(causes CURLE_WRITE_ERROR) buf += std::string(buffer, size * nmemb); return size * nmemb; } int main() { const std::string url = "https://www.uuidtools.com/api/generate/v1"; std::string buf; // buffer to receive content // as it assumes the content is text, response body can't include '\0's // setup curl std::shared_ptr<CURL> curl(curl_easy_init(), curl_easy_cleanup); curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, curl_callback); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &buf); // perform request auto res = curl_easy_perform(curl.get()); if (res != CURLE_OK) { if (res == CURLE_WRITE_ERROR) { throw std::runtime_error("CURLE_WRITE_ERROR: Content size limit exceeded?"); } else { throw std::runtime_error(std::string(curl_easy_strerror(res)) + "(" + std::to_string(res) + ")"); } } // collect HTTP status code long http_code = 0; curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &http_code); // collect Content-Type char *ct = nullptr; res = curl_easy_getinfo(curl.get(), CURLINFO_CONTENT_TYPE, &ct); if (res != CURLE_OK) throw std::runtime_error(curl_easy_strerror(res)); std::optional<std::string> content_type = ct? std::make_optional(ct) : std::nullopt; // check status code and content type if (http_code != 200) { throw std::runtime_error("HTTP status other than 200 OK received: status code=" + std::to_string(http_code)); } if (content_type != "application/json") { throw std::runtime_error("Not application/json: " + content_type.value_or("N/A")); } // parse json char errorbuf[1024]; std::shared_ptr<yajl_val_s> tree(yajl_tree_parse(buf.c_str(), errorbuf, sizeof(errorbuf)), yajl_tree_free); if (!tree) throw std::runtime_error(std::string("yajl_tree_parse() failed: ") + errorbuf); if (!YAJL_IS_ARRAY(tree)) throw std::runtime_error("Not a JSON array"); auto array = YAJL_GET_ARRAY(tree); if (array->len < 1) throw std::runtime_error("Array length is zero"); auto value = array->values[0]; if (!YAJL_IS_STRING(value)) throw std::runtime_error("Array item is not a string"); // print item std::cout << YAJL_GET_STRING(value) << std::endl; return 0; } // g++ -std=c++20 -o curl_get_json curl_get_json.cpp -lcurl -lyajl ``` [^1]: libcurlには [curlpp](http://www.curlpp.org/) という C++ラッパーもあるようだが、これはどこででも利用できるライブラリというわけではないのでここでは使用しない。