mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 21:37:45 +00:00
* WIP: Haskell impl of resizeImageToStrSize * WIP: add FFI export wrapper * WIP: switch ios resizeImageToStrSizeSync to backend * WIP: fix resizeImageToStrSizeSync * WIP: add JNI wrappers * Add non-base64 resize and cWriteImage * WIP: add chat_write_image * fix resizeToStr * fixes * fix syntax, raise jpeg quality back * add chatWriteImage * Fix cfArgs in tests * Use jpeg-turbo encoder * add nix pins * bump jpeg-turbo * add CI deps * add encoding funs to android lib * bump jpeg-turbo for 20.04 fix * jpeg-turbo fix for 8.10 * android fixes * Bump jpeg-turbo * set static flag for jpeg-turbo * Fix nix for aarch64/android * Fix android C api * Fix resize in android * Bump hs-libjpeg-turbo * target correct override * override android lib from android pkgs * add macos build tweaks * bump hs-jpeg-turbo for 8.10 fixes * more build fixes * make ios compile * Always use pkgconfig for libjpeg-turbo * WIP: debug libjpeg in ubuntu pkg-config CI * Add pkg-config to dockerfiles * Add libturbojpeg to dockerfiles * Use -dev version of libturbojpeg in dockerfiles * Install pkg-config in windows CI * Disable flag for custom libturbojpeg linking options Workarounds relocation error on x86_64-linux with gcc: /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libturbojpeg.a(turbojpeg.c.o): relocation R_X86_64_TPOFF32 against `errStr' can not be used when making a shared object; recompile with -fPIC /usr/bin/ld: failed to set dynamic section sizes: bad value * WIP: debug libjpeg in windows pkg-config CI * WIP: debug libjpeg package in windows CI * core: image resize module and buffer FFI * ci: link libjpeg-turbo statically on all platforms * ui: resize images in the core * core: rename image resize functions to chat_resize_image and chat_resize_image_uri * ci: restore whitespace in build.yml * ci: build libjpeg-turbo 3.1.4.1 from a submodule * core: hs-jpeg-turbo from github.com/simplex-chat * nodejs: resize images * python: resize images * core: hs-jpeg-turbo from gitlab.com, the GitHub import lacks LFS objects * core: generate /img previews * ci: link libjpeg-turbo on Windows without pkg-config * ci: build libjpeg-turbo on Windows with the GHC toolchain * docs: plan for image resizing in the core * docs: update image resizing plan * ios: prevent crash when wallpaper image cannot be resized * android, desktop: raise an error when an image cannot be resized * docs: update spec references for image resizing * docker: build the CLI image from a clone without submodules * docs: apply review fixes to image resizing plan * docker: build the CLI from a git worktree with the submodule present * docs: apply second review to image resizing plan * docs: point image resizing plan at the rewritten commit * core: choose image format by transparency in chat_resize_image * nodejs: resize images to the app sizes * python: resize images to the app sizes * nodejs, python: describe the core modules as used for image resizing * docs: add license notices of the image libraries * ci: describe the avatar method without attributing it to the apps * ci, ios, nix: remove the libjpeg-turbo build outside cabal * python: name resize_image in the core module text * nodejs, python: describe resizing to the app image sizes * core: refuse images larger than the supported size * core: resize to JPEG images with a side above 65500 pixels * docs: record image resizing decisions for the bundled libjpeg-turbo * core, docs: build libjpeg-turbo and stb from the fork submodules * docs: record the fork pins in the image resizing plan * core: resize images to at most 4320 pixels per side, refuse above 16384 * docs: record the image side limits in the image resizing plan * core: pin the rebased hs-jpeg-turbo fork * core: pin the hs-jpeg-turbo fork without package.yaml * docs: plan scaled JPEG decoding and EXIF orientation of images * docs: copy the JuicyPixels-stbir licence of the pinned fork * core: pin the hs-jpeg-turbo fork with scaled JPEG decoding * core: decode JPEG at the display size, up to 65500 pixels per side * core: rotate JPEG images by their EXIF orientation * docs: refer the image plan to the JPEG decoding code * docs: record the approved image processing decisions * core: limit JPEG decoding memory to 1024 MB outside iOS and Android * docs: name JPEG decoding in the libjpeg-turbo notice * docs: plan image resizing from files and the decisions of 26 Oct * core: scale images in one Catmull-Rom pass with sharpening * docs: plan image sizes at a fixed quality * core: resize images at a fixed quality to fill the size limit * core: resize images from a file path or a file descriptor * ios, android, desktop: resize picked images from their files * core: encode resized images with mozjpeg * tests: check descriptor closing by the file it refers to * core: raise JPEG quality only for a full-size image below 97% of the limit * ci: name the mozjpeg submodule in the Windows cleanup comments * docs: refer the image plan to the current code * docs: refer the image plan to the code merged with master * core: export every image resizing function from each library * core: close the file descriptor when its handle cannot be created * docs: count the C sources of mozjpeg with the arithmetic decoder * core: use the reviewed hs-jpeg-turbo and JuicyPixels-stbir forks * ci: build the armv7a Android core library on request * core: round image heights half up in the size search * ios: describe the file image resizing functions in SimpleX.h * ci: pick, preview and send images in the Claude app tests * core: decode PNG images with libspng and refuse other formats * core: encode PNG with libspng, keeping the smaller filter choice * ci: pick, preview and send images in the scripted app smoke tests * flake: build the PNG decoder package in nix builds * ios: remove the copy of a picked image after sending it * core: refuse image files larger than the decode memory limit * core: scale a JPEG image to its largest size once per resize * core: bisect JPEG quality without a step function * core: reduce the memory used to sharpen and to decode PNG images * android, desktop: pass the image format to the core as an integer * android, desktop: fuse the preview conditions of picked media * android, desktop, ci: check that the core resized the smoke test images * python: remove the docstrings of the explicit image resizing functions * docs: add the second review fixes to the image plan * ci: tap the iOS attachment button by its accessibility label * core: keep the colour profiles of resized images * docs: add the colour profiles to the image plan * ci: wait up to 30 s for the new group screens in the iOS smoke test * ci: pick the Android smoke test images from their own photo folders * ci: tap the iOS photo picker by position, as its tree is not shown * ci: pick the test card and the logo by their iOS picker positions * core: update JuicyPixels-stbir with the stb_image_resize licence terms * core: write a fixed Display P3 profile for Display P3 images only * core: do not keep the input image in memory for its colour profile * core: read the JPEG segments once for the orientation and the profile * core: read an image file into one buffer of its size * docs: add the third review fixes to the image plan * core: resize images with Display P3 or no profile, not profile bytes * ci: check the pixel sizes of the images sent in the iOS smoke test * core: update hs-jpeg-turbo with PNG chunk limits and encoder fixes * docs: list Compact ICC Profiles, libspng and miniz in the dependencies * android, desktop: reword the picked media preview comment * docs: refer the image plan to the code merged with master * ci: render the iOS snapshots before the images case * ci: dismiss the snapshot tests' camera alert before the iOS images case * core: update hs-jpeg-turbo with tests and the JuicyPixels-spng synopsis * core: pin hs-jpeg-turbo and JuicyPixels-stbir in simplex-chat repositories * update ios core library * core, ui: accept only JPEG and PNG image data * core: update JuicyPixels-stbir * docs: plan for the iOS copy of picked images --------- Co-authored-by: IC Rainbow <aenor.realm@gmail.com> Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> Co-authored-by: Alexandre Esteves <alexfmpe@proton.me>
898 lines
29 KiB
C++
898 lines
29 KiB
C++
#include <napi.h>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <functional>
|
|
#include <cstdlib>
|
|
#include <climits>
|
|
#include <memory>
|
|
#include <thread>
|
|
#include <system_error>
|
|
#include <mutex>
|
|
#include <atomic>
|
|
#include <condition_variable>
|
|
#include <deque>
|
|
#include <unordered_map>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#else
|
|
#include <dlfcn.h>
|
|
#endif
|
|
#include "simplex.h"
|
|
|
|
namespace simplex {
|
|
|
|
using namespace Napi;
|
|
|
|
struct Library {
|
|
std::string path;
|
|
hs_init_with_rtsopts_fn hs_init_with_rtsopts = nullptr;
|
|
hs_thread_done_fn hs_thread_done = nullptr;
|
|
chat_migrate_init_fn chat_migrate_init = nullptr;
|
|
chat_migrate_init_queue_fn chat_migrate_init_queue = nullptr;
|
|
chat_close_store_fn chat_close_store = nullptr;
|
|
chat_send_cmd_fn chat_send_cmd = nullptr;
|
|
chat_recv_msg_wait_fn chat_recv_msg_wait = nullptr;
|
|
chat_write_file_fn chat_write_file = nullptr;
|
|
chat_read_file_fn chat_read_file = nullptr;
|
|
chat_encrypt_file_fn chat_encrypt_file = nullptr;
|
|
chat_decrypt_file_fn chat_decrypt_file = nullptr;
|
|
chat_resize_image_fn chat_resize_image = nullptr;
|
|
chat_resize_image_uri_fn chat_resize_image_uri = nullptr;
|
|
};
|
|
|
|
static Library lib;
|
|
static std::atomic<bool> loaded{false};
|
|
static std::mutex load_mutex;
|
|
|
|
#ifdef _WIN32
|
|
typedef HMODULE LibHandle;
|
|
|
|
static LibHandle OpenLibrary(const std::string& path) {
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, nullptr, 0);
|
|
std::wstring wpath(n, L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, path.c_str(), -1, &wpath[0], n);
|
|
// dependencies resolve from the libsimplex.dll, application and system dirs, not the cwd or PATH
|
|
return LoadLibraryExW(wpath.c_str(), nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
|
|
}
|
|
|
|
static void CloseLibrary(LibHandle h) {
|
|
FreeLibrary(h);
|
|
}
|
|
|
|
static void* FindSymbol(LibHandle h, const char* name) {
|
|
return reinterpret_cast<void*>(GetProcAddress(h, name));
|
|
}
|
|
|
|
static std::string LastLoadError() {
|
|
DWORD code = GetLastError();
|
|
char* msg = nullptr;
|
|
FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
|
nullptr, code, 0, reinterpret_cast<LPSTR>(&msg), 0, nullptr);
|
|
std::string err = msg ? msg : "error " + std::to_string(code);
|
|
LocalFree(msg);
|
|
// system messages end with CRLF
|
|
err.erase(err.find_last_not_of("\r\n") + 1);
|
|
return err;
|
|
}
|
|
#else
|
|
typedef void* LibHandle;
|
|
|
|
static LibHandle OpenLibrary(const std::string& path) {
|
|
return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
|
}
|
|
|
|
static void CloseLibrary(LibHandle h) {
|
|
dlclose(h);
|
|
}
|
|
|
|
static void* FindSymbol(LibHandle h, const char* name) {
|
|
return dlsym(h, name);
|
|
}
|
|
|
|
static std::string LastLoadError() {
|
|
const char* err = dlerror();
|
|
return err ? err : "unknown error";
|
|
}
|
|
#endif
|
|
|
|
template <typename F>
|
|
static void Resolve(LibHandle h, const char* name, F& fn) {
|
|
fn = reinterpret_cast<F>(FindSymbol(h, name));
|
|
}
|
|
|
|
template <typename F>
|
|
static void ResolveRequired(LibHandle h, const char* name, F& fn, std::string& missing) {
|
|
Resolve(h, name, fn);
|
|
if (fn == nullptr && missing.empty()) missing = name;
|
|
}
|
|
|
|
void haskell_init() {
|
|
#ifdef _WIN32
|
|
// non-moving GC is broken on windows with GHC 9.4-9.6.3
|
|
int argc = 5;
|
|
const char *argv[] = {
|
|
"simplex",
|
|
"+RTS", // requires `hs_init_with_rtsopts`
|
|
"-A64m", // chunk size for new allocations
|
|
"-H64m", // initial heap size
|
|
"--install-signal-handlers=no",
|
|
nullptr};
|
|
#else
|
|
int argc = 6;
|
|
const char *argv[] = {
|
|
"simplex",
|
|
"+RTS", // requires `hs_init_with_rtsopts`
|
|
"-A64m", // chunk size for new allocations
|
|
"-H64m", // initial heap size
|
|
"-xn", // non-moving GC
|
|
"--install-signal-handlers=no",
|
|
nullptr};
|
|
#endif
|
|
char **pargv = const_cast<char **>(argv);
|
|
lib.hs_init_with_rtsopts(&argc, &pargv);
|
|
}
|
|
|
|
static bool RequireLoaded(Env env) {
|
|
if (loaded) return true;
|
|
Error::New(env, "libsimplex is not loaded, call core.loadLibrary(backend) first").ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
|
|
template <typename F>
|
|
static bool RequireExport(Env env, F fn, const std::string& name, const std::string& feature) {
|
|
if (fn != nullptr) return true;
|
|
Error::New(env, "loaded libsimplex does not export " + name + "; " + feature + " needs a newer libsimplex").ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
|
|
static bool GetInt32Arg(Env env, const Value& value, const std::string& name, int& result) {
|
|
Number number = value.As<Number>();
|
|
result = number.Int32Value();
|
|
if (static_cast<double>(result) == number.DoubleValue()) return true;
|
|
RangeError::New(env, "Expected 32-bit integer " + name).ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
|
|
struct BufferArg {
|
|
char* data;
|
|
int len;
|
|
};
|
|
|
|
static bool GetBufferArg(Env env, const Value& value, BufferArg& result) {
|
|
size_t len;
|
|
if (value.IsArrayBuffer()) {
|
|
ArrayBuffer ab = value.As<ArrayBuffer>();
|
|
result.data = static_cast<char*>(ab.Data());
|
|
len = ab.ByteLength();
|
|
} else {
|
|
TypedArray view = value.As<TypedArray>();
|
|
result.data = static_cast<char*>(view.ArrayBuffer().Data()) + view.ByteOffset();
|
|
len = view.ByteLength();
|
|
}
|
|
if (len > static_cast<size_t>(INT_MAX)) {
|
|
RangeError::New(env, "Buffer is too large").ThrowAsJavaScriptException();
|
|
return false;
|
|
}
|
|
result.len = static_cast<int>(len);
|
|
return true;
|
|
}
|
|
|
|
Value Load(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (args.Length() < 1 || !args[0].IsString()) {
|
|
TypeError::New(env, "Expected string (libPath)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
std::string path = args[0].As<String>().Utf8Value();
|
|
// worker_threads may load concurrently; the Haskell runtime must be initialized once per process
|
|
std::lock_guard<std::mutex> lock(load_mutex);
|
|
if (loaded) {
|
|
if (path != lib.path) Error::New(env, "libsimplex already loaded from " + lib.path).ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
LibHandle h = OpenLibrary(path);
|
|
if (h == nullptr) {
|
|
std::string err = LastLoadError();
|
|
Error::New(env, "cannot load " + path + ": " + err).ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
Library l;
|
|
l.path = path;
|
|
std::string missing;
|
|
ResolveRequired(h, "hs_init_with_rtsopts", l.hs_init_with_rtsopts, missing);
|
|
ResolveRequired(h, "chat_migrate_init", l.chat_migrate_init, missing);
|
|
ResolveRequired(h, "chat_close_store", l.chat_close_store, missing);
|
|
ResolveRequired(h, "chat_send_cmd", l.chat_send_cmd, missing);
|
|
ResolveRequired(h, "chat_recv_msg_wait", l.chat_recv_msg_wait, missing);
|
|
ResolveRequired(h, "chat_write_file", l.chat_write_file, missing);
|
|
ResolveRequired(h, "chat_read_file", l.chat_read_file, missing);
|
|
ResolveRequired(h, "chat_encrypt_file", l.chat_encrypt_file, missing);
|
|
ResolveRequired(h, "chat_decrypt_file", l.chat_decrypt_file, missing);
|
|
if (!missing.empty()) {
|
|
CloseLibrary(h);
|
|
Error::New(env, path + " does not export " + missing).ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
Resolve(h, "chat_migrate_init_queue", l.chat_migrate_init_queue);
|
|
Resolve(h, "chat_resize_image", l.chat_resize_image);
|
|
Resolve(h, "chat_resize_image_uri", l.chat_resize_image_uri);
|
|
Resolve(h, "hs_thread_done", l.hs_thread_done);
|
|
lib = l;
|
|
haskell_init();
|
|
loaded = true;
|
|
return env.Undefined();
|
|
}
|
|
|
|
class ResultAsyncWorker : public AsyncWorker {
|
|
public:
|
|
using ExecuteFn = std::function<void(ResultAsyncWorker*)>;
|
|
using ResultProcessor = std::function<void(ResultAsyncWorker*, Napi::Env)>;
|
|
|
|
ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr)
|
|
: AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {}
|
|
|
|
void Execute() override {
|
|
execute_fn_(this);
|
|
}
|
|
|
|
void OnOK() override {
|
|
HandleScope scope(Env());
|
|
if (result_processor_) {
|
|
result_processor_(this, Env());
|
|
} else {
|
|
Callback().Call({Env().Null(), String::New(Env(), result_)});
|
|
}
|
|
}
|
|
|
|
void OnError(const Error& e) override {
|
|
HandleScope scope(Env());
|
|
Callback().Call({e.Value(), Env().Undefined()});
|
|
}
|
|
|
|
void SetResult(std::string result) {
|
|
result_ = std::move(result);
|
|
}
|
|
|
|
void SetWorkerError(const std::string& msg) {
|
|
SetError(msg);
|
|
}
|
|
|
|
const std::string& GetStringResult() const {
|
|
return result_;
|
|
}
|
|
|
|
void SetCtrl(uintptr_t ctrl) {
|
|
ctrl_ = ctrl;
|
|
}
|
|
|
|
uintptr_t GetCtrl() const {
|
|
return ctrl_;
|
|
}
|
|
|
|
// the worker thread reads this object's memory until the worker completes
|
|
void KeepAlive(Object obj) {
|
|
keep_alive_ = Persistent(obj);
|
|
}
|
|
|
|
protected:
|
|
std::string result_;
|
|
uintptr_t ctrl_ = 0;
|
|
|
|
private:
|
|
ExecuteFn execute_fn_;
|
|
ResultProcessor result_processor_;
|
|
ObjectReference keep_alive_;
|
|
};
|
|
|
|
class BinaryAsyncWorker : public AsyncWorker {
|
|
public:
|
|
using ExecuteFn = std::function<void(BinaryAsyncWorker*)>;
|
|
|
|
BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn)
|
|
: AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {}
|
|
|
|
~BinaryAsyncWorker() {
|
|
free(original_buf);
|
|
}
|
|
|
|
void Execute() override {
|
|
execute_fn_(this);
|
|
}
|
|
|
|
void OnOK() override {
|
|
HandleScope scope(Env());
|
|
char* buf = original_buf;
|
|
original_buf = nullptr;
|
|
if (binary_len == 0) {
|
|
free(buf);
|
|
Callback().Call({Env().Null(), Buffer<char>::New(Env(), 0)});
|
|
return;
|
|
}
|
|
// Copies when the runtime forbids external buffers (Electron); the finalizer then runs immediately.
|
|
Buffer<char> buffer = Buffer<char>::NewOrCopy(Env(), buf + 5, binary_len, [](Napi::Env, char*, char* orig) { free(orig); }, buf);
|
|
Callback().Call({Env().Null(), buffer});
|
|
}
|
|
|
|
void OnError(const Error& e) override {
|
|
HandleScope scope(Env());
|
|
Callback().Call({e.Value(), Env().Undefined()});
|
|
}
|
|
|
|
void SetWorkerError(const std::string& msg) {
|
|
SetError(msg);
|
|
}
|
|
|
|
char* original_buf = nullptr;
|
|
size_t binary_len = 0;
|
|
|
|
private:
|
|
ExecuteFn execute_fn_;
|
|
};
|
|
|
|
// Helper for converting chat_ctrl pointer to BigInt
|
|
Napi::BigInt ToChatCtrlBigInt(Napi::Env env, uintptr_t ctrl) {
|
|
return Napi::BigInt::New(env, static_cast<uint64_t>(ctrl));
|
|
}
|
|
|
|
// Helper for converting BigInt to chat_ctrl pointer
|
|
chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) {
|
|
Napi::Env env = value.Env();
|
|
if (!value.IsBigInt()) {
|
|
Napi::TypeError::New(env, "Expected BigInt for ctrl").ThrowAsJavaScriptException();
|
|
return nullptr;
|
|
}
|
|
Napi::BigInt big = value.As<Napi::BigInt>();
|
|
bool lossless;
|
|
uint64_t val = big.Uint64Value(&lossless);
|
|
if (!lossless) {
|
|
Napi::TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException();
|
|
return nullptr;
|
|
}
|
|
return reinterpret_cast<chat_ctrl>(val);
|
|
}
|
|
|
|
// Helper for handling common C result patterns (no empty check)
|
|
void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) {
|
|
if (c_res == nullptr) {
|
|
worker->SetWorkerError(func_name + " failed");
|
|
return;
|
|
}
|
|
std::string res = c_res;
|
|
free(c_res);
|
|
worker->SetResult(res);
|
|
}
|
|
|
|
Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) {
|
|
Promise::Deferred deferred = Promise::Deferred::New(env);
|
|
cb_out = Function::New(env, [deferred](const CallbackInfo& args) {
|
|
if (!args[0].IsNull() && !args[0].IsUndefined()) {
|
|
deferred.Reject(args[0]);
|
|
} else {
|
|
deferred.Resolve(args[1]);
|
|
}
|
|
});
|
|
return deferred.Promise();
|
|
}
|
|
|
|
const char* const RECEIVER_STOPPED = "chat receiver stopped";
|
|
|
|
struct RecvRequest {
|
|
int wait = 0;
|
|
std::shared_ptr<Promise::Deferred> deferred;
|
|
};
|
|
|
|
// Holds the event loop open only while receives are pending; used only on the JS main thread.
|
|
class PendingReceives {
|
|
public:
|
|
explicit PendingReceives(ThreadSafeFunction tsfn) : tsfn_(tsfn) {}
|
|
|
|
const ThreadSafeFunction& Tsfn() const {
|
|
return tsfn_;
|
|
}
|
|
|
|
void Add(Napi::Env env) {
|
|
if (count_++ == 0) tsfn_.Ref(env);
|
|
}
|
|
|
|
void Remove(Napi::Env env) {
|
|
if (--count_ == 0) tsfn_.Unref(env);
|
|
}
|
|
|
|
private:
|
|
ThreadSafeFunction tsfn_;
|
|
size_t count_ = 0;
|
|
};
|
|
|
|
// A blocking receive would hold a libuv pool thread for up to `wait`, stalling fs, dns and crypto.
|
|
class Receiver {
|
|
public:
|
|
// Returns nullptr with a pending JS exception if the TSFN cannot be created, throws std::system_error if the thread cannot start.
|
|
static std::shared_ptr<Receiver> Start(Napi::Env env, chat_ctrl ctrl) {
|
|
ThreadSafeFunction tsfn = ThreadSafeFunction::New(env, Function::New(env, [](const CallbackInfo&) {}), "chat_recv_msg_wait", 0, 1);
|
|
if (env.IsExceptionPending()) {
|
|
return nullptr;
|
|
}
|
|
auto receiver = std::make_shared<Receiver>(ctrl, tsfn);
|
|
try {
|
|
receiver->thread_ = std::thread(&Receiver::Run, receiver.get());
|
|
} catch (const std::system_error&) {
|
|
tsfn.Release();
|
|
throw;
|
|
}
|
|
return receiver;
|
|
}
|
|
|
|
Receiver(chat_ctrl ctrl, ThreadSafeFunction tsfn) : ctrl_(ctrl), pending_(std::make_shared<PendingReceives>(tsfn)) {}
|
|
|
|
Receiver(const Receiver&) = delete;
|
|
Receiver& operator=(const Receiver&) = delete;
|
|
|
|
~Receiver() {
|
|
Stop();
|
|
}
|
|
|
|
void Enqueue(Napi::Env env, RecvRequest request) {
|
|
pending_->Add(env);
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
queue_.push_back(std::move(request));
|
|
}
|
|
cv_.notify_one();
|
|
}
|
|
|
|
void RequestStop() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
stop_ = true;
|
|
}
|
|
cv_.notify_one();
|
|
}
|
|
|
|
// Waits for the receive in progress, so it must not run on the JS main thread outside env teardown.
|
|
void Stop() {
|
|
RequestStop();
|
|
if (thread_.joinable()) {
|
|
thread_.join();
|
|
}
|
|
}
|
|
|
|
private:
|
|
void Run() {
|
|
for (;;) {
|
|
RecvRequest request;
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
cv_.wait(lock, [this] { return stop_ || !queue_.empty(); });
|
|
if (stop_) break;
|
|
request = std::move(queue_.front());
|
|
queue_.pop_front();
|
|
}
|
|
char* c_res = lib.chat_recv_msg_wait(ctrl_, request.wait);
|
|
napi_status status = Settle(request.deferred, [c_res](Napi::Env env, Promise::Deferred& deferred) {
|
|
if (c_res == nullptr) {
|
|
deferred.Reject(Error::New(env, "chat_recv_msg_wait failed").Value());
|
|
} else {
|
|
deferred.Resolve(String::New(env, c_res));
|
|
free(c_res);
|
|
}
|
|
});
|
|
if (status != napi_ok) {
|
|
free(c_res);
|
|
}
|
|
}
|
|
std::deque<RecvRequest> unserved;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
unserved.swap(queue_);
|
|
}
|
|
for (RecvRequest& request : unserved) {
|
|
Settle(request.deferred, [](Napi::Env env, Promise::Deferred& deferred) {
|
|
deferred.Reject(Error::New(env, RECEIVER_STOPPED).Value());
|
|
});
|
|
}
|
|
pending_->Tsfn().Release();
|
|
// Each OS thread that enters Haskell keeps an RTS task record until it calls hs_thread_done.
|
|
if (lib.hs_thread_done) lib.hs_thread_done();
|
|
}
|
|
|
|
template <typename SettleFn>
|
|
napi_status Settle(std::shared_ptr<Promise::Deferred> deferred, SettleFn settle) {
|
|
// The callback may run after this Receiver is destroyed, so it owns the pending count.
|
|
std::shared_ptr<PendingReceives> pending = pending_;
|
|
return pending->Tsfn().BlockingCall([pending, deferred, settle](Napi::Env env, Function) {
|
|
settle(env, *deferred);
|
|
pending->Remove(env);
|
|
});
|
|
}
|
|
|
|
const chat_ctrl ctrl_;
|
|
const std::shared_ptr<PendingReceives> pending_;
|
|
std::mutex mutex_;
|
|
std::condition_variable cv_;
|
|
std::deque<RecvRequest> queue_;
|
|
bool stop_ = false;
|
|
std::thread thread_;
|
|
};
|
|
|
|
// Keyed by chat_ctrl, accessed only on the JS main thread.
|
|
using Receivers = std::unordered_map<uintptr_t, std::shared_ptr<Receiver>>;
|
|
|
|
std::shared_ptr<Receiver> TakeReceiver(Receivers& receivers, chat_ctrl ctrl) {
|
|
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
|
if (it == receivers.end()) {
|
|
return nullptr;
|
|
}
|
|
std::shared_ptr<Receiver> receiver = std::move(it->second);
|
|
receivers.erase(it);
|
|
return receiver;
|
|
}
|
|
|
|
// Common result processors
|
|
ResultAsyncWorker::ResultProcessor MigrateResultProcessor() {
|
|
return [](ResultAsyncWorker* worker, Napi::Env env) {
|
|
Napi::Array arr = Napi::Array::New(env, 2);
|
|
arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl()));
|
|
arr.Set(1u, Napi::String::New(env, worker->GetStringResult()));
|
|
worker->Callback().Call({env.Null(), arr});
|
|
};
|
|
}
|
|
|
|
// Refactored functions using common patterns
|
|
|
|
Value ChatMigrateInit(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
|
|
TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
std::string path = args[0].As<String>().Utf8Value();
|
|
std::string key = args[1].As<String>().Utf8Value();
|
|
std::string confirm = args[2].As<String>().Utf8Value();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) {
|
|
chat_ctrl ctrl = nullptr;
|
|
char* c_res = lib.chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl);
|
|
worker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
|
HandleCResult(worker, c_res, "chat_migrate_init");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatMigrateInitQueue(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (!RequireExport(env, lib.chat_migrate_init_queue, "chat_migrate_init_queue", "queue size")) return env.Undefined();
|
|
if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsNumber()) {
|
|
TypeError::New(env, "Expected three string arguments and number").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
std::string path = args[0].As<String>().Utf8Value();
|
|
std::string key = args[1].As<String>().Utf8Value();
|
|
std::string confirm = args[2].As<String>().Utf8Value();
|
|
int queue_size;
|
|
if (!GetInt32Arg(env, args[3], "queue size", queue_size)) return env.Undefined();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [path, key, confirm, queue_size](ResultAsyncWorker* worker) {
|
|
chat_ctrl ctrl = nullptr;
|
|
char* c_res = lib.chat_migrate_init_queue(path.c_str(), key.c_str(), confirm.c_str(), queue_size, &ctrl);
|
|
worker->SetCtrl(reinterpret_cast<uintptr_t>(ctrl));
|
|
HandleCResult(worker, c_res, "chat_migrate_init_queue");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor());
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatCloseStore(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 1 || !args[0].IsBigInt()) {
|
|
TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
std::shared_ptr<Receiver> receiver = TakeReceiver(*static_cast<Receivers*>(args.Data()), ctrl);
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [ctrl, receiver](ResultAsyncWorker* worker) {
|
|
if (receiver) {
|
|
receiver->Stop();
|
|
}
|
|
char* c_res = lib.chat_close_store(ctrl);
|
|
HandleCResult(worker, c_res, "chat_close_store");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatSendCmd(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) {
|
|
TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
std::string cmd = args[1].As<String>().Utf8Value();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_send_cmd(ctrl, cmd.c_str());
|
|
HandleCResult(worker, c_res, "chat_send_cmd");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatRecvMsgWait(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) {
|
|
TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
int wait = static_cast<int>(args[1].As<Number>().Int32Value());
|
|
Receivers& receivers = *static_cast<Receivers*>(args.Data());
|
|
|
|
auto deferred = std::make_shared<Promise::Deferred>(Promise::Deferred::New(env));
|
|
auto it = receivers.find(reinterpret_cast<uintptr_t>(ctrl));
|
|
if (it == receivers.end()) {
|
|
std::shared_ptr<Receiver> receiver;
|
|
try {
|
|
receiver = Receiver::Start(env, ctrl);
|
|
} catch (const std::system_error& e) {
|
|
deferred->Reject(Error::New(env, e.what()).Value());
|
|
return deferred->Promise();
|
|
}
|
|
if (!receiver) {
|
|
return env.Undefined();
|
|
}
|
|
it = receivers.emplace(reinterpret_cast<uintptr_t>(ctrl), std::move(receiver)).first;
|
|
}
|
|
it->second->Enqueue(env, {wait, deferred});
|
|
|
|
return deferred->Promise();
|
|
}
|
|
|
|
Value ChatWriteFile(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !(args[2].IsArrayBuffer() || args[2].IsTypedArray())) {
|
|
TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer or Uint8Array").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
std::string path = args[1].As<String>().Utf8Value();
|
|
BufferArg buffer;
|
|
if (!GetBufferArg(env, args[2], buffer)) return env.Undefined();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [ctrl, path, buffer](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_write_file(ctrl, path.c_str(), buffer.data, buffer.len);
|
|
HandleCResult(worker, c_res, "chat_write_file");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->KeepAlive(args[2].As<Object>());
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatReadFile(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) {
|
|
TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
std::string path = args[0].As<String>().Utf8Value();
|
|
std::string key = args[1].As<String>().Utf8Value();
|
|
std::string nonce = args[2].As<String>().Utf8Value();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [path, key, nonce](BinaryAsyncWorker* worker) {
|
|
char* buf = lib.chat_read_file(path.c_str(), key.c_str(), nonce.c_str());
|
|
if (buf == nullptr) {
|
|
worker->SetWorkerError("chat_read_file failed");
|
|
return;
|
|
}
|
|
char status = buf[0];
|
|
if (status == 1) {
|
|
std::string err = buf + 1;
|
|
free(buf);
|
|
worker->SetWorkerError(err);
|
|
return;
|
|
} else if (status == 0) {
|
|
uint32_t len = *(uint32_t*)(buf + 1);
|
|
worker->original_buf = buf;
|
|
worker->binary_len = len;
|
|
} else {
|
|
free(buf);
|
|
worker->SetWorkerError("Unexpected status from chat_read_file");
|
|
return;
|
|
}
|
|
};
|
|
|
|
BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_fn));
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatEncryptFile(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) {
|
|
TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
chat_ctrl ctrl = FromChatCtrlBigInt(args[0]);
|
|
std::string fromPath = args[1].As<String>().Utf8Value();
|
|
std::string toPath = args[2].As<String>().Utf8Value();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str());
|
|
HandleCResult(worker, c_res, "chat_encrypt_file");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatDecryptFile(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) {
|
|
TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
std::string fromPath = args[0].As<String>().Utf8Value();
|
|
std::string key = args[1].As<String>().Utf8Value();
|
|
std::string nonce = args[2].As<String>().Utf8Value();
|
|
std::string toPath = args[3].As<String>().Utf8Value();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str());
|
|
HandleCResult(worker, c_res, "chat_decrypt_file");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatResizeImage(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (!RequireExport(env, lib.chat_resize_image, "chat_resize_image", "image resizing")) return env.Undefined();
|
|
if (args.Length() < 3 || !(args[0].IsArrayBuffer() || args[0].IsTypedArray()) || !args[1].IsNumber() || !args[2].IsNumber()) {
|
|
TypeError::New(env, "Expected ArrayBuffer or Uint8Array, number (maxSize), number (png)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
BufferArg image;
|
|
if (!GetBufferArg(env, args[0], image)) return env.Undefined();
|
|
int max_size;
|
|
if (!GetInt32Arg(env, args[1], "maxSize", max_size)) return env.Undefined();
|
|
int png;
|
|
if (!GetInt32Arg(env, args[2], "png", png)) return env.Undefined();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [image, max_size, png](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_resize_image(image.data, image.len, max_size, png);
|
|
HandleCResult(worker, c_res, "chat_resize_image");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->KeepAlive(args[0].As<Object>());
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Value ChatResizeImageUri(const CallbackInfo& args) {
|
|
Env env = args.Env();
|
|
if (!RequireLoaded(env)) return env.Undefined();
|
|
if (!RequireExport(env, lib.chat_resize_image_uri, "chat_resize_image_uri", "image resizing")) return env.Undefined();
|
|
if (args.Length() < 2 || !(args[0].IsArrayBuffer() || args[0].IsTypedArray()) || !args[1].IsNumber()) {
|
|
TypeError::New(env, "Expected ArrayBuffer or Uint8Array, number (maxSize)").ThrowAsJavaScriptException();
|
|
return env.Undefined();
|
|
}
|
|
|
|
BufferArg image;
|
|
if (!GetBufferArg(env, args[0], image)) return env.Undefined();
|
|
int max_size;
|
|
if (!GetInt32Arg(env, args[1], "maxSize", max_size)) return env.Undefined();
|
|
|
|
Function cb;
|
|
Promise promise = CreatePromiseAndCallback(env, cb);
|
|
|
|
auto execute_fn = [image, max_size](ResultAsyncWorker* worker) {
|
|
char* c_res = lib.chat_resize_image_uri(image.data, image.len, max_size);
|
|
HandleCResult(worker, c_res, "chat_resize_image_uri");
|
|
};
|
|
|
|
ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn));
|
|
worker->KeepAlive(args[0].As<Object>());
|
|
worker->Queue();
|
|
|
|
return promise;
|
|
}
|
|
|
|
Object Init(Env env, Object exports) {
|
|
exports.Set("load", Function::New(env, Load));
|
|
auto* receivers = new Receivers();
|
|
// Stopping all receivers before joining any bounds teardown by the longest in-flight receive.
|
|
env.AddCleanupHook([receivers]() {
|
|
for (auto& entry : *receivers) {
|
|
entry.second->RequestStop();
|
|
}
|
|
delete receivers;
|
|
});
|
|
exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit));
|
|
exports.Set("chat_migrate_init_queue", Function::New(env, ChatMigrateInitQueue));
|
|
exports.Set("chat_close_store", Function::New(env, ChatCloseStore, "chat_close_store", receivers));
|
|
exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd));
|
|
exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait, "chat_recv_msg_wait", receivers));
|
|
exports.Set("chat_write_file", Function::New(env, ChatWriteFile));
|
|
exports.Set("chat_read_file", Function::New(env, ChatReadFile));
|
|
exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile));
|
|
exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile));
|
|
exports.Set("chat_resize_image", Function::New(env, ChatResizeImage));
|
|
exports.Set("chat_resize_image_uri", Function::New(env, ChatResizeImageUri));
|
|
return exports;
|
|
}
|
|
|
|
NODE_API_MODULE(simplex, Init)
|
|
|
|
}
|