Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Curl #61

Merged
merged 16 commits into from
Jul 27, 2024
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ jobs:
run: |
sudo apt install -y ninja-build \
nlohmann-json3-dev \
libwebkit2gtk-4.1-dev
libwebkit2gtk-4.1-dev \
libcurl4-openssl-dev
# npx playwright install-deps
- name: Configure (macOS)
Expand Down
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ endif()

find_package(PkgConfig REQUIRED)
pkg_check_modules(NlohmannJson REQUIRED IMPORTED_TARGET "nlohmann_json")
find_package(CURL REQUIRED)

if(LINUX)
pkg_check_modules(webkit2gtk REQUIRED IMPORTED_TARGET "webkit2gtk-4.1")
set(LIBS PkgConfig::NlohmannJson PkgConfig::webkit2gtk)
set(LIBS PkgConfig::NlohmannJson PkgConfig::webkit2gtk CURL::libcurl)
elseif(APPLE)
find_library(COCOA_LIB Cocoa REQUIRED)
find_library(WEBKIT_LIB WebKit REQUIRED)
set(LIBS PkgConfig::NlohmannJson ${COCOA_LIB} ${WEBKIT_LIB})
set(LIBS PkgConfig::NlohmannJson ${COCOA_LIB} ${WEBKIT_LIB} CURL::libcurl)
endif()

include_directories(pugixml/src)
Expand Down
54 changes: 54 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# API

## `curl` (async)

With `curl`, you can interact with network resources directly.

```ts
async function curl(url: string, args: CurlArgs) => CurlResponse

type CurlArgs = {
method?: "GET" | "POST" | "DELETE" | "HEAD" | "OPTIONS" | "PUT" | "PATCH",
headers?: object,
data?: string, // ignored if `json` exists
json?: JSON,
binary?: bool,
timeout?: uint64 // milliseconds
}

type CurlResponse = {
status: number,
data: string,
}
```

- If `args.binary` is `true`, then `response.data` will be a base64-encoded representation of the original data.
- The precision of `args.timeout` is 50ms.

**Example** POST w/ JSON:

```js
# Basic usage
curl("https://httpbin.org/post", {
json: {arr: [1,2,3]}
}).then(r => {
console.log(r.status, r.status==200)
return JSON.parse(r.data)
}).then(j => console.log(j.data))

# OpenAI
curl("https://api.openai.com/v1/chat/completions", {
headers: { "Authorization": "Bearer $OPENAI_API_KEY" },
json: {
"model": "gpt-4o-mini",
"messages": [{
"role": "system",
"content": "You are a poetic assistant, skilled in explaining complex programming concepts with creative flair."
}, {
"role": "user",
"content": "Compose a poem that explains the concept of recursion in programming."
}]
}
}).then(r => JSON.parse(r.data))
.then(j => console.log(j))
```
27 changes: 27 additions & 0 deletions include/curl.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <curl/curl.h>
#include <functional>
#include <shared_mutex>
#include <thread>

class CurlMultiManager {
public:
using Callback = std::function<void(CURLcode, CURL *, const std::string &)>;
using HandleData = std::pair<CURL *, Callback>;

static CurlMultiManager &shared();
CurlMultiManager();
~CurlMultiManager();
void add(CURL *easy, CurlMultiManager::Callback cb);

private:
CURLM *multi;
std::thread worker_thread;
int controlfd[2];
std::shared_mutex m;
std::unordered_map<CURL *, std::string> buf;
std::unordered_map<CURL *, Callback> cb;

void run();
};
1 change: 1 addition & 0 deletions include/utility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ template <typename T> function_traits<T> make_function_traits(const T &) {
}

std::string escape_html(const std::string &content);
std::string base64(const std::string &s);
9 changes: 9 additions & 0 deletions include/webview_candidate_window.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#include <sstream>

namespace candidate_window {

enum CustomAPI : uint64_t { kCurl = 1 };

class WebviewCandidateWindow : public CandidateWindow {
public:
WebviewCandidateWindow();
Expand All @@ -33,6 +36,8 @@ class WebviewCandidateWindow : public CandidateWindow {
void set_accent_color();
void copy_html();

void set_api(uint64_t apis);

private:
std::shared_ptr<webview::webview> w_;
double cursor_x_ = 0;
Expand All @@ -58,6 +63,10 @@ class WebviewCandidateWindow : public CandidateWindow {
void *platform_data = nullptr;
void platform_init();

private:
/* API */
void api_curl(std::string id, std::string req);

private:
/* Invoke a JavaScript function. */
template <typename Ret = void, bool debug = false, typename... Args>
Expand Down
10 changes: 6 additions & 4 deletions preview/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
add_executable(preview preview.cpp)
target_link_libraries(preview WebviewCandidateWindow)
target_include_directories(preview PRIVATE "${PROJECT_SOURCE_DIR}/include")

if(APPLE)
add_executable(preview MACOSX_BUNDLE preview.cpp)
target_compile_options(preview PRIVATE "-Wno-auto-var-id" "-ObjC++")
else()
add_executable(preview preview.cpp)
endif()

target_link_libraries(preview WebviewCandidateWindow)
target_include_directories(preview PRIVATE "${PROJECT_SOURCE_DIR}/include")

add_dependencies(preview GenerateHTML)
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ add_library(WebviewCandidateWindow
"${PROJECT_SOURCE_DIR}/pugixml/src/pugixml.cpp"
webview_candidate_window.cpp
platform.cpp
curl.cpp
)
target_include_directories(WebviewCandidateWindow PUBLIC "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}/webview")
target_link_libraries(WebviewCandidateWindow ${LIBS})
Expand Down
134 changes: 134 additions & 0 deletions src/curl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#include "curl.hpp"
#include <atomic>
#include <cassert>
#include <errno.h>
#include <mutex>
#include <stdexcept>
#include <thread>
#include <unistd.h>

static size_t _on_data_cb(char *data, size_t size, size_t nmemb,
std::string *outbuf);
static bool write_char_strong(int fd, char c);
static bool read_char_strong(int fd, char *c);
std::mutex m;
std::atomic<bool> running;

CurlMultiManager &CurlMultiManager::shared() {
static CurlMultiManager instance;
return instance;
}

CurlMultiManager::CurlMultiManager() {
bool expected = false;
if (!running.compare_exchange_strong(expected, true)) {
throw std::runtime_error("should only run one curl manager");
}
if (pipe(controlfd) < 0) {
throw std::runtime_error("failed to create curl control pipe");
}
curl_global_init(CURL_GLOBAL_ALL);
multi = curl_multi_init();
worker_thread = std::thread(&CurlMultiManager::run, this);
}

CurlMultiManager::~CurlMultiManager() {
write_char_strong(controlfd[1], 'q');
if (worker_thread.joinable()) {
worker_thread.join();
}
curl_multi_cleanup(multi);
curl_global_cleanup();
close(controlfd[0]);
close(controlfd[1]);
running.store(false);
}

void CurlMultiManager::add(CURL *easy, CurlMultiManager::Callback callback) {
{
std::unique_lock g(m);
buf[easy] = "";
cb[easy] = callback;
}
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, _on_data_cb);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, &buf[easy]);
curl_multi_add_handle(multi, easy);
std::atomic_thread_fence(std::memory_order_release);
write_char_strong(controlfd[1], 'a');
}

void CurlMultiManager::run() {
while (true) {
int still_running = 0;
int ret;
ret = curl_multi_perform(multi, &still_running);
int numfds;
struct curl_waitfd wfd;
wfd.fd = controlfd[0];
wfd.events = CURL_WAIT_POLLIN;
wfd.revents = 0;
// NOTE: poll does not return when any of the easy handle's
// timeout expires. By setting poll's timeout to 50ms, we
// effectively set the timeout precision to 50ms.
curl_multi_poll(multi, &wfd, 1, 50, &numfds);
std::atomic_thread_fence(std::memory_order_acquire);
if (wfd.revents) {
char cmd;
read_char_strong(controlfd[0], &cmd);
switch (cmd) {
case 'q': // quit
return;
case 'a': // added a new handle
break;
default:
assert(false && "unreachable");
}
}
CURLMsg *msg;
int msgs_left;
while ((msg = curl_multi_info_read(multi, &msgs_left))) {
if (msg->msg == CURLMSG_DONE) {
CURL *easy = msg->easy_handle;
CURLcode res = msg->data.result;
{
// do this because the cb might be slow
std::shared_lock g(m);
try {
cb[easy](res, easy, buf[easy]);
} catch (...) {
assert(false && "curl callback must not throw!");
}
}
curl_multi_remove_handle(multi, easy);
curl_easy_cleanup(easy);
{
std::unique_lock g(m);
cb.erase(easy);
buf.erase(easy);
}
}
}
}
}

size_t _on_data_cb(char *data, size_t size, size_t nmemb, std::string *outbuf) {
size_t realsize = size * nmemb;
outbuf->append(data, realsize);
return realsize;
}

static bool write_char_strong(int fd, char c) {
ssize_t ret;
do {
ret = write(fd, &c, 1);
} while (ret == -1 && errno == EINTR);
return ret == 1;
}

static bool read_char_strong(int fd, char *c) {
ssize_t ret;
do {
ret = read(fd, c, 1);
} while (ret == -1 && errno == EINTR);
return ret == 1;
}
22 changes: 22 additions & 0 deletions src/utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,25 @@ std::string escape_html(const std::string &content) {
doc.print(ss, "", pugi::format_raw);
return ss.str();
}

std::string base64(const std::string &s) {
static const char *chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string ret;
ret.reserve((s.size() + 2) / 3 * 4);
unsigned int w = 0;
int b = -6;
for (unsigned char c : s) {
w = (w << 8) + c;
b += 8;
while (b >= 0) {
ret += chars[(w >> b) & 0x3F];
b -= 6;
}
}
if (b > -6)
ret += chars[((w << 8) >> (b + 8)) & 0x3F];
while (ret.size() % 4)
ret += '=';
return ret;
}
Loading