Skip to content

Commit

Permalink
Curl (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksqsf authored Jul 27, 2024
1 parent e0ae870 commit 452209e
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 7 deletions.
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

0 comments on commit 452209e

Please sign in to comment.