kit: add C++ generator, add native-explorer extension

extension-host: load local extensions
This commit is contained in:
DH
2025-08-25 15:42:01 +03:00
parent 00734c6f87
commit 6ee300f26b
40 changed files with 28544 additions and 961 deletions

View File

@@ -8,6 +8,14 @@ updates:
deps:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/electron"
schedule:
interval: "weekly"
groups:
deps:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/"
schedule:
@@ -15,4 +23,4 @@ updates:
groups:
deps:
patterns:
- "*"
- "*"

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ expo-env.d.ts
.expo/
android/
/electron/out/
extensions/cpp/rpcsx-ui

2
extensions/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build*/
.cache/

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.30)
project(rpcsx-ui-extensions)
set(CMAKE_CXX_STANDARD 23)
add_subdirectory(rpcsx-ui)
add_subdirectory(rpcsx-ui-cpp)
add_subdirectory(explorer)

View File

@@ -0,0 +1,4 @@
add_rpcsx_extension(native-explorer 0.1.0
src/extension.cpp
src/sfo.cpp
)

View File

@@ -0,0 +1,29 @@
{
"name": [
{
"text": "@EXTENSION_NAME@"
}
],
"version": "@EXTENSION_VERSION@",
"executable": "@EXTENSION_EXECUTABLE@",
"args": [],
"launcher": {
"type": "@EXTENSION_TARGET@",
"requirements": {}
},
"contributions": {
"settings": {
"locations": {
"type": "array",
"required": true,
"description": "Search locations",
"minItems": 1,
"items": {
"type": "path",
"mustExist": true,
"entity": "directory"
}
}
}
}
}

View File

@@ -0,0 +1,217 @@
#include "./sfo.hpp"
#include <print>
#include <rpcsx/ui/extension.hpp>
#include <thread>
using namespace rpcsx::ui;
static std::size_t calcDirectorySize(const std::filesystem::path &path) {
std::uint64_t result = 0;
for (auto dir : std::filesystem::recursive_directory_iterator(path)) {
if (dir.is_regular_file()) {
result += std::filesystem::file_size(dir);
}
}
return result;
}
static std::optional<ExplorerItem>
tryFetchFw(const std::filesystem::directory_entry &entry) {
if (!std::filesystem::is_regular_file(entry.path() / "mini-syscore.elf")) {
return {};
}
if (!std::filesystem::is_regular_file(entry.path() / "safemode.elf")) {
return {};
}
if (!std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
"SceSysCore.elf")) {
return {};
}
if (!std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
"orbis_audiod.elf")) {
return {};
}
if (std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
"GnmCompositor.elf")) {
return ExplorerItem{
.type = "firmware",
.name = {LocalizedString{
.text = "PS4 Firmware",
}},
.location = "file://" + entry.path().string(),
.size = calcDirectorySize(entry.path()),
.launcher =
LauncherInfo{
.type = "dir-ps4-fw",
},
};
}
if (std::filesystem::is_regular_file(entry.path() / "system" / "sys" /
"AgcCompositor.elf")) {
return ExplorerItem{
.type = "firmware",
.name = {LocalizedString{
.text = "PS5 Firmware",
}},
.location = "file://" + entry.path().string(),
.size = calcDirectorySize(entry.path()),
.launcher =
LauncherInfo{
.type = "dir-ps5-fw",
},
};
}
return {};
}
static std::optional<ExplorerItem>
tryFetchGame(const std::filesystem::directory_entry &entry) {
if (!entry.is_directory()) {
return {};
}
auto sysPath = entry.path() / "sce_sys";
auto paramSfoPath = sysPath / "param.sfo";
if (!std::filesystem::is_regular_file(entry.path() / "eboot.bin")) {
return {};
}
if (!std::filesystem::is_regular_file(paramSfoPath)) {
return {};
}
auto data = sfo::load(paramSfoPath);
if (data.errc != sfo::error::ok) {
std::println(stderr, "{}: error {}", entry.path().string(), data.errc);
return {};
}
auto category = sfo::get_string(data.sfo, "CATEGORY");
if (category == "gdd" || category == "gdf" || category == "gdp" ||
category == "gdg") {
return {};
}
ExplorerItem info;
auto name = sfo::get_string(data.sfo, "TITLE");
if (name.empty()) {
name = sfo::get_string(data.sfo, "TITLE_ID");
if (name.empty()) {
return {};
}
}
info.name = {LocalizedString{.text = std::string(name)}};
info.titleId = sfo::get_string(data.sfo, "TITLE_ID");
info.version = sfo::get_string(data.sfo, "APP_VER");
if (info.version->empty()) {
info.version = sfo::get_string(data.sfo, "VERSION", "1.0");
}
if (std::filesystem::is_regular_file(sysPath / "icon0.png")) {
info.icon = {
LocalizedIcon{.uri = "file://" + (sysPath / "icon0.png").string()}};
}
info.size = calcDirectorySize(entry.path());
info.type = "game";
info.launcher = LauncherInfo{
.type = "fself-ps4-orbis" // FIXME: self/elf? ps3? ps5?
// "fself-ps5-prospero"
};
info.location = "file://" + entry.path().string();
return std::move(info);
}
struct ExplorerExtension : rpcsx::ui::Extension<rpcsx::ui::Explorer> {
std::jthread explorerThread;
std::vector<std::string> locations;
using Base::Base;
Response<Initialize> handle(const Request<Initialize> &) override {
return {};
}
Response<Activate> handle(const Request<Activate> &request) override {
std::fprintf(stderr, "activate request, settings = %s\n",
json(request.settings).dump().c_str());
settingsGet({.path = "/"},
[](const rpcsx::ui::SettingsGetResponse &response) {
std::fprintf(stderr, "settings: schema: %s\n",
nlohmann::json(response.schema).dump().c_str());
std::fprintf(stderr, "settings: value: %s\n",
response.value.dump().c_str());
});
if (!request.settings.contains("locations")) {
return {};
}
locations = request.settings.at("locations");
explorerThread = std::jthread([this](std::stop_token token) {
ExplorerItem batchItems[8];
std::size_t batchSize = 0;
auto flush = [&] {
if (batchSize > 0) {
this->explorerAdd({.items = {batchItems, batchItems + batchSize}});
batchSize = 0;
}
};
auto submit = [&](ExplorerItem item) {
if (batchSize >= std::size(batchItems)) {
flush();
}
batchItems[batchSize++] = std::move(item);
};
for (auto &location : locations) {
for (auto &entry :
std::filesystem::recursive_directory_iterator(location)) {
if (token.stop_requested()) {
return;
}
if (auto game = tryFetchGame(entry)) {
submit(std::move(*game));
continue;
}
if (auto fw = tryFetchFw(entry)) {
submit(std::move(*fw));
continue;
}
}
}
flush();
});
return {};
}
Response<Shutdown> handle(const Request<Shutdown> &) override { return {}; }
};
auto extension_main() {
return rpcsx::ui::createExtension<ExplorerExtension>();
}

View File

@@ -0,0 +1,280 @@
#include "sfo.hpp"
#include "rpcsx/ui/file.hpp"
#include "rpcsx/ui/format.hpp"
#include "rpcsx/ui/log.hpp"
#include <cassert>
#include <cstring>
#include <print>
#include <source_location>
#include <span>
#include <utility>
using namespace rpcsx::ui;
template <typename T> using le_t = T;
struct header_t {
le_t<std::uint32_t> magic;
le_t<std::uint32_t> version;
le_t<std::uint32_t> off_key_table;
le_t<std::uint32_t> off_data_table;
le_t<std::uint32_t> entries_num;
};
static_assert(sizeof(header_t) == 20);
struct def_table_t {
le_t<std::uint16_t> key_off;
le_t<sfo::format> param_fmt;
le_t<std::uint32_t> param_len;
le_t<std::uint32_t> param_max;
le_t<std::uint32_t> data_off;
};
static_assert(sizeof(def_table_t) == 16);
sfo::entry::entry(format type, std::uint32_t max_size, std::string_view value,
bool allow_truncate) noexcept
: m_format(type), m_max_size(max_size), m_value_string(value) {
assert(type == format::string || type == format::array);
assert(max_size > (type == format::string ? 1u : 0u));
if (allow_truncate && value.size() > max(false)) {
m_value_string.resize(max(false));
}
}
sfo::entry::entry(std::uint32_t value) noexcept
: m_format(format::integer), m_max_size(sizeof(std::uint32_t)),
m_value_integer(value) {}
const std::string &sfo::entry::as_string() const {
assert(m_format == format::string || m_format == format::array);
return m_value_string;
}
std::uint32_t sfo::entry::as_integer() const {
assert(m_format == format::integer);
return m_value_integer;
}
sfo::entry &sfo::entry::operator=(std::string_view value) {
assert(m_format == format::string || m_format == format::array);
m_value_string = value;
return *this;
}
sfo::entry &sfo::entry::operator=(std::uint32_t value) {
assert(m_format == format::integer);
m_value_integer = value;
return *this;
}
std::uint32_t sfo::entry::size() const {
switch (m_format) {
case format::string:
case format::array:
return std::min(m_max_size, static_cast<std::uint32_t>(
m_value_string.size() +
(m_format == format::string ? 1 : 0)));
case format::integer:
return sizeof(std::uint32_t);
}
fatal("sfo: invalid format ({})", m_format);
}
bool sfo::entry::is_valid() const {
switch (m_format) {
case format::string:
case format::array:
return m_value_string.size() <= this->max(false);
case format::integer:
return true;
}
return false;
}
sfo::load_result_t sfo::load(ReadableByteStream stream,
std::string_view filename) {
load_result_t result{};
#define PSF_CHECK(cond, err) \
if (!static_cast<bool>(cond)) { \
if (true || err != error::stream) \
elog("sfo: Error loading '{}': {}. {}", filename, err, \
std::source_location::current()); \
result.sfo.clear(); \
result.errc = err; \
return result; \
}
auto originalStream = stream;
PSF_CHECK(!stream.empty(), error::stream);
// Get header
header_t header;
PSF_CHECK(stream.read(header), error::not_psf);
// Check magic and version
le_t<std::uint32_t> expMagic;
std::memcpy(&expMagic, "\0PSF", sizeof(expMagic));
PSF_CHECK(header.magic == expMagic, error::not_psf);
PSF_CHECK(header.version == 0x101u, error::not_psf);
PSF_CHECK(header.off_key_table >= sizeof(header_t), error::corrupt);
PSF_CHECK(header.off_key_table <= header.off_data_table, error::corrupt);
PSF_CHECK(header.off_data_table <= stream.size(), error::corrupt);
// Get indices
std::vector<def_table_t> indices;
PSF_CHECK(stream.read(indices, header.entries_num), error::corrupt);
// Get keys
std::string keys;
PSF_CHECK(originalStream.size() > header.off_key_table, error::corrupt);
stream = originalStream.subspan(header.off_key_table);
PSF_CHECK(stream.read(keys, header.off_data_table - header.off_key_table),
error::corrupt);
// Load entries
for (std::uint32_t i = 0; i < header.entries_num; ++i) {
PSF_CHECK(indices[i].key_off < header.off_data_table - header.off_key_table,
error::corrupt);
// Get key name (null-terminated string)
std::string_view key(keys.data() + indices[i].key_off);
// Check entry
PSF_CHECK(!result.sfo.contains(key), error::corrupt);
PSF_CHECK(indices[i].param_len <= indices[i].param_max, error::corrupt);
PSF_CHECK(indices[i].data_off <
originalStream.size() - header.off_data_table,
error::corrupt);
PSF_CHECK(indices[i].param_max <
originalStream.size() - indices[i].data_off,
error::corrupt);
// Seek data pointer
PSF_CHECK(originalStream.size() >
header.off_data_table + indices[i].data_off,
error::corrupt);
stream =
originalStream.subspan(header.off_data_table + indices[i].data_off);
if (indices[i].param_fmt == format::integer &&
indices[i].param_max == sizeof(std::uint32_t) &&
indices[i].param_len == sizeof(std::uint32_t)) {
// Integer data
le_t<std::uint32_t> value;
PSF_CHECK(stream.read(value), error::corrupt);
result.sfo.emplace(std::piecewise_construct,
std::forward_as_tuple(std::move(key)),
std::forward_as_tuple(value));
} else if (indices[i].param_fmt == format::string ||
indices[i].param_fmt == format::array) {
// String/array data
std::string value;
PSF_CHECK(stream.read(value, indices[i].param_len), error::corrupt);
if (indices[i].param_fmt == format::string) {
// Find null terminator
value.resize(std::strlen(value.c_str()));
}
result.sfo.emplace(
std::piecewise_construct, std::forward_as_tuple(std::move(key)),
std::forward_as_tuple(indices[i].param_fmt, indices[i].param_max,
std::move(value)));
} else {
// Possibly unsupported format, entry ignored
elog("sfo: Unknown entry format (key='{}', fmt={}, len=0x{:x}, "
"max=0x{:x})",
key, indices[i].param_fmt, indices[i].param_len,
indices[i].param_max);
}
}
#undef PSF_CHECK
return result;
}
sfo::load_result_t sfo::load(const std::string &filename) {
auto file = File::open(filename);
if (!file.has_value()) {
std::println(stderr, "file open error {}", file.error().message());
return {{}, error::stream};
}
auto data = file->map();
if (!data.has_value()) {
std::println(stderr, "file map error {}", data.error().message());
return {{}, error::stream};
}
return load(data.value(), filename);
}
std::string_view sfo::get_string(const registry &psf, std::string_view key,
std::string_view def) {
const auto found = psf.find(key);
if (found == psf.end() || (found->second.type() != format::string &&
found->second.type() != format::array)) {
return def;
}
return found->second.as_string();
}
std::uint32_t sfo::get_integer(const registry &psf, std::string_view key,
std::uint32_t def) {
const auto found = psf.find(key);
if (found == psf.end() || found->second.type() != format::integer) {
return def;
}
return found->second.as_integer();
}
bool sfo::check_registry(
const registry &psf,
std::function<bool(bool ok, const std::string &key, const entry &value)>
validate,
std::source_location src_loc) {
bool psf_ok = true;
for (const auto &[key, value] : psf) {
bool entry_ok = value.is_valid();
if (validate) {
// Validate against a custom condition as well (forward error)
if (!validate(entry_ok, key, value)) {
entry_ok = false;
}
}
if (!entry_ok) {
if (value.type() == format::string) {
elog("sfo: {}: Entry '{}' is invalid: string='{}'", src_loc, key,
value.as_string());
} else {
// TODO: Better logging of other types
elog("sfo: {}: Entry {} is invalid", src_loc, key, value.as_string());
}
}
if (!entry_ok) {
// Do not break, run over all entries in order to report all errors
psf_ok = false;
}
}
return psf_ok;
}

View File

@@ -0,0 +1,256 @@
#pragma once
#include "rpcsx/ui/file.hpp"
#include "rpcsx/ui/refl.hpp"
#include <cstddef>
#include <cstdint>
#include <format>
#include <functional>
#include <map>
#include <source_location>
#include <string>
#include <string_view>
#include <vector>
namespace sfo {
enum sound_format_flag : std::int32_t {
lpcm_2 = 1 << 0, // Linear PCM 2 Ch.
lpcm_5_1 = 1 << 2, // Linear PCM 5.1 Ch.
lpcm_7_1 = 1 << 4, // Linear PCM 7.1 Ch.
ac3 = 1 << 8, // Dolby Digital 5.1 Ch.
dts = 1 << 9, // DTS 5.1 Ch.
};
enum resolution_flag : std::int32_t {
_480 = 1 << 0,
_576 = 1 << 1,
_720 = 1 << 2,
_1080 = 1 << 3,
_480_16_9 = 1 << 4,
_576_16_9 = 1 << 5,
};
enum class format : std::uint16_t {
array = 0x0004, // claimed to be a non-NTS string (char array)
string = 0x0204,
integer = 0x0404,
};
enum class error {
ok,
stream,
not_psf,
corrupt,
};
class entry final {
format m_format{};
std::uint32_t
m_max_size{}; // Entry max size (supplementary info, stored in PSF format)
std::uint32_t m_value_integer{}; // TODO: is it really unsigned?
std::string m_value_string{};
public:
// Construct string entry, assign the value
entry(format type, std::uint32_t max_size, std::string_view value,
bool allow_truncate = false) noexcept;
// Construct integer entry, assign the value
entry(std::uint32_t value) noexcept;
~entry() = default;
const std::string &as_string() const;
std::uint32_t as_integer() const;
entry &operator=(std::string_view value);
entry &operator=(std::uint32_t value);
format type() const { return m_format; }
std::uint32_t max(bool with_nts) const {
return m_max_size - (!with_nts && m_format == format::string ? 1 : 0);
}
std::uint32_t size() const;
bool is_valid() const;
};
// Define PSF registry as a sorted map of entries:
using registry = std::map<std::string, entry, std::less<>>;
struct load_result_t {
registry sfo;
error errc;
explicit operator bool() const { return !sfo.empty(); }
};
// Load PSF registry from SFO binary format
load_result_t load(rpcsx::ui::ReadableByteStream data,
std::string_view filename);
load_result_t load(const std::string &filename);
inline registry load_object(rpcsx::ui::ReadableByteStream data,
std::string_view filename) {
return load(data, filename).sfo;
}
inline registry load_object(const std::string &filename) {
return load(filename).sfo;
}
// Convert PSF registry to SFO binary format
std::vector<std::uint8_t>
save_object(const registry &,
std::vector<std::uint8_t> &&init = std::vector<std::uint8_t>{});
// Get string value or default value
std::string_view get_string(const registry &psf, std::string_view key,
std::string_view def = {});
// Get integer value or default value
std::uint32_t get_integer(const registry &psf, std::string_view key,
std::uint32_t def = 0);
bool check_registry(
const registry &psf,
std::function<bool(bool ok, const std::string &key, const entry &value)>
validate = {},
std::source_location src_loc = std::source_location::current());
// Assign new entry
inline void assign(registry &psf, std::string_view key, entry &&_entry) {
const auto found = psf.find(key);
if (found == psf.end()) {
psf.emplace(key, std::move(_entry));
return;
}
found->second = std::move(_entry);
return;
}
// Make string entry
inline entry string(std::uint32_t max_size, std::string_view value,
bool allow_truncate = false) {
return {format::string, max_size, value, allow_truncate};
}
// Make string entry (from char[N])
template <std::size_t CharN>
inline entry string(std::uint32_t max_size, char (&value_array)[CharN],
bool allow_truncate = false) {
std::string_view value{value_array, CharN};
value = value.substr(
0, std::min<std::size_t>(value.find_first_of('\0'), value.size()));
return string(max_size, value, allow_truncate);
}
// Make array entry
inline entry array(std::uint32_t max_size, std::string_view value) {
return {format::array, max_size, value};
}
// Checks if of HDD category (assumes a valid category is being passed)
constexpr bool is_cat_hdd(std::string_view cat) {
return cat.size() == 2u && cat[1] != 'D' && cat != "DG" && cat != "MS";
}
} // namespace sfo
template <> struct std::formatter<sfo::format> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
constexpr std::format_context::iterator
format(sfo::format format, std::format_context &ctx) const {
std::string_view name;
switch (format) {
case sfo::format::array:
name = rpcsx::ui::getNameOf<sfo::format::array>();
break;
case sfo::format::string:
name = rpcsx::ui::getNameOf<sfo::format::string>();
break;
case sfo::format::integer:
name = rpcsx::ui::getNameOf<sfo::format::integer>();
break;
}
if (name.empty()) {
std::format_to(ctx.out(), "({}){:#x}",
rpcsx::ui::getNameOf<sfo::format>(),
std::to_underlying(format));
return ctx.out();
}
std::format_to(ctx.out(), "{}", name);
return ctx.out();
}
};
template <> struct std::formatter<std::source_location> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
constexpr std::format_context::iterator
format(const std::source_location &location, std::format_context &ctx) const {
std::format_to(ctx.out(), "{}:{}", location.file_name(), location.line());
return ctx.out();
}
};
template <> struct std::formatter<sfo::registry> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
constexpr std::format_context::iterator
format(const sfo::registry &registry, std::format_context &ctx) const {
for (const auto &entry : registry) {
if (entry.second.type() == sfo::format::array) {
// Format them last
continue;
}
std::format_to(ctx.out(), "{}: ", entry.first);
const sfo::entry &data = entry.second;
if (data.type() == sfo::format::integer) {
std::format_to(ctx.out(), "0x{:x}\n", data.as_integer());
} else {
std::format_to(ctx.out(), "\"{}\"\n", data.as_string());
}
}
for (const auto &entry : registry) {
if (entry.second.type() != sfo::format::array) {
// Formatted before
continue;
}
std::format_to(ctx.out(), "{}: [", entry.first);
for (bool first = true; auto byte : std::span<const std::uint8_t>(
reinterpret_cast<const std::uint8_t *>(
entry.second.as_string().data()),
entry.second.size())) {
if (first) {
first = false;
} else {
std::format_to(ctx.out(), ", ");
}
std::format_to(ctx.out(), "{:x}", byte);
}
std::format_to(ctx.out(), "]\n");
}
return ctx.out();
}
};

View File

@@ -0,0 +1,3 @@
add_library(nlohmann_json INTERFACE)
target_include_directories(nlohmann_json INTERFACE single_include)
add_library(nlohmann::json ALIAS nlohmann_json)

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-2025 Niels Lohmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.11.3
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.11.3
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

View File

@@ -0,0 +1,39 @@
add_subdirectory(3rdparty/nlohmann_json)
add_library(
rpcsx-ui-cpp STATIC
src/extension.cpp
src/file.cpp
)
target_include_directories(rpcsx-ui-cpp PUBLIC include)
target_link_libraries(rpcsx-ui-cpp PUBLIC nlohmann::json rpcsx::ui)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" EXTENSION_TRIPLE_ARCH)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" EXTENSION_TRIPLE_OS)
if("${EXTENSION_TRIPLE_OS}" STREQUAL "windows")
set(EXTENSION_TRIPLE_FORMAT "pe")
else()
set(EXTENSION_TRIPLE_FORMAT "elf")
endif()
if("${EXTENSION_TRIPLE_ARCH}" STREQUAL "x86_64")
set(EXTENSION_TRIPLE_ARCH "x64")
endif()
set(EXTENSION_TARGET "${EXTENSION_TRIPLE_FORMAT}-${EXTENSION_TRIPLE_ARCH}-${EXTENSION_TRIPLE_OS}" PARENT_SCOPE)
function(add_rpcsx_extension name version)
set(EXTENSION_NAME ${name})
set(EXTENSION_VERSION ${version})
set(EXTENSION_EXECUTABLE ${EXTENSION_NAME})
add_executable(${EXTENSION_EXECUTABLE} ${ARGN})
target_link_libraries(${EXTENSION_EXECUTABLE} PUBLIC rpcsx-ui-cpp)
target_compile_definitions(${EXTENSION_EXECUTABLE} PUBLIC EXTENSION_NAME="${EXTENSION_NAME}" EXTENSION_VERSION="${EXTENSION_VERSION}")
set_target_properties(${EXTENSION_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
make_directory(${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
configure_file(extension.json ${CMAKE_BINARY_DIR}/bin/${EXTENSION_NAME})
endfunction()

View File

@@ -0,0 +1,59 @@
#pragma once
#include "Transport.hpp"
#include <functional>
#include <nlohmann/json_fwd.hpp>
#include <rpcsx-ui.hpp>
#include <string_view>
namespace rpcsx::ui {
using json = nlohmann::json;
struct ExtensionBase;
class Protocol {
Transport *mTransport = nullptr;
ExtensionBase *mHandlers = nullptr;
public:
Protocol() = default;
Protocol(Transport *transport) : mTransport(transport) {}
template <typename... Components> void registerComponents() {}
void setHandlers(ExtensionBase *handlers) { mHandlers = handlers; }
ExtensionBase &getHandlers() { return *mHandlers; }
virtual ~Protocol() = default;
virtual void call(std::string_view method, json params,
std::function<void(json)> responseHandler) = 0;
virtual void notify(std::string_view method, json params) = 0;
virtual void onEvent(std::string_view method,
std::function<void(json)> eventHandler) = 0;
virtual int processMessages() = 0;
virtual void sendLogMessage(LogLevel level, std::string_view message) = 0;
virtual void sendResponse(std::size_t id, json result) = 0;
virtual void sendErrorResponse(std::size_t id, ErrorInstance error) = 0;
virtual void sendErrorResponse(ErrorInstance error) = 0;
virtual void
addMethodHandler(std::string_view method,
std::function<void(std::size_t id, json body)> handler) = 0;
virtual void
addNotificationHandler(std::string_view notification,
std::function<void(json body)> handler) = 0;
static Protocol *getDefault() { return *getImpl(); }
static void setDefault(Protocol *protocol) { *getImpl() = protocol; }
Transport *getTransport() { return mTransport; }
private:
static Protocol **getImpl() {
static Protocol *protocol = nullptr;
return &protocol;
}
};
} // namespace rpcsx::ui

View File

@@ -0,0 +1,12 @@
#pragma once
#include <span>
namespace rpcsx::ui {
class Transport {
public:
virtual ~Transport() = default;
virtual void write(std::span<const std::byte> bytes) = 0;
virtual void read(std::span<std::byte> &bytes) = 0;
virtual void flush() {}
};
} // namespace rpcsx::ui

View File

@@ -0,0 +1,51 @@
#pragma once
#include "Protocol.hpp" // IWYU pragma: export
#include "format.hpp" // IWYU pragma: export
#include "refl.hpp" // IWYU pragma: export
#include <expected>
#include <rpcsx-ui.hpp>
namespace rpcsx::ui {
template <typename T>
using Response = std::expected<typename T::Response, ErrorInstance>;
template <typename T> using Request = T::Request;
struct ExtensionBase {
virtual ~ExtensionBase() = default;
virtual Response<Initialize> handle(const Request<Initialize> &) {
return {};
}
virtual Response<Activate> handle(const Request<Activate> &) { return {}; }
virtual Response<Shutdown> handle(const Request<Shutdown> &) { return {}; }
};
template <typename... Components>
class Extension
: public ExtensionBase,
public Core::instance<Extension<Components...>>,
public Components::template instance<Extension<Components...>>... {
Protocol *m_protocol = nullptr;
public:
using Base = Extension;
Extension() = default;
Extension(Protocol *protocol) : m_protocol(protocol) {
m_protocol->template registerComponents<Components...>();
m_protocol->setHandlers(this);
}
Protocol &getProtocol() const { return *m_protocol; }
};
using ExtensionBuilder =
std::function<std::unique_ptr<ExtensionBase>(Protocol *)>;
template <typename T> ExtensionBuilder createExtension() {
auto builder = [](Protocol *protocol) {
return std::make_unique<T>(protocol);
};
return builder;
}
} // namespace rpcsx::ui

View File

@@ -0,0 +1,98 @@
#pragma once
#include <cstring>
#include <expected>
#include <filesystem>
#include <ios>
#include <span>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
namespace rpcsx::ui {
struct ReadableByteStream : std::span<const std::byte> {
using base = std::span<const std::byte>;
using base::base;
using base::operator=;
bool read(void *dest, std::size_t bytes) {
if (size() < bytes) {
return false;
}
std::memcpy(dest, data(), bytes);
*this = subspan(bytes);
return true;
}
template <typename T>
requires std::is_trivially_copyable_v<T>
bool read(T &target) {
return read(&target, sizeof(target));
}
template <typename T>
requires std::is_trivially_copyable_v<T>
bool read(std::vector<T> &target, std::size_t count) {
target.resize(count);
return read(target.data(), sizeof(T) * count);
}
bool read(std::string &target, std::size_t count) {
target.resize(count);
return read(target.data(), count);
}
};
struct FileData : std::span<std::byte> {
struct Impl;
FileData() = default;
FileData(const FileData &) = delete;
FileData &operator=(const FileData &) = delete;
FileData(FileData &&other) : std::span<std::byte>(other), m_impl(std::exchange(other.m_impl, nullptr)) {
static_cast<std::span<std::byte> &>(other) = std::span<std::byte>{};
}
FileData &operator=(FileData &&other) {
std::swap(m_impl, other.m_impl);
std::swap(static_cast<std::span<std::byte> &>(*this),
static_cast<std::span<std::byte> &>(other));
return *this;
}
~FileData();
FileData(Impl *i, std::span<std::byte> data)
: std::span<std::byte>(data), m_impl(i) {}
private:
Impl *m_impl = nullptr;
};
struct FileStat {};
struct File {
struct Impl;
File() = default;
File(const File &) = delete;
File &operator=(const File &) = delete;
File(File &&other) : m_impl(std::exchange(other.m_impl, nullptr)) {}
File &operator=(File &&other) {
std::swap(m_impl, other.m_impl);
return *this;
}
~File();
std::expected<FileData, std::error_code> map();
static std::expected<File, std::error_code>
open(const std::filesystem::path &path,
std::ios::openmode mode = std::ios::binary | std::ios::in);
private:
Impl *m_impl = nullptr;
};
} // namespace rpcsx::ui

View File

@@ -0,0 +1,319 @@
#pragma once
#include "refl.hpp"
#include <array>
#include <format>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include <utility>
namespace rpcsx::ui {
namespace detail {
struct StructFieldInfo {
std::size_t size = 0;
std::size_t align = 0;
std::size_t offset = 0;
std::format_context::iterator (*format)(void *,
std::format_context &ctx) = nullptr;
std::string_view name;
};
struct StructFieldQuery {
StructFieldInfo info;
template <typename T> constexpr operator T() {
info.size = sizeof(T);
info.align = alignof(T);
if constexpr (std::is_default_constructible_v<std::formatter<T>>) {
info.format =
[](void *object,
std::format_context &ctx) -> std::format_context::iterator {
std::formatter<T> formatter;
return formatter.format(*static_cast<T *>(object), ctx);
};
}
return {};
}
};
template <typename StructT, typename T>
std::size_t getFieldOffset(T StructT::*ptr) {
StructT queryStruct;
return std::bit_cast<std::byte *>(&(queryStruct.*ptr)) -
std::bit_cast<std::byte *>(&queryStruct);
}
template <typename> struct StructRuntimeInfo {
std::unordered_map<std::size_t, std::string_view> fieldInfo;
template <auto Field> void registerField() {
fieldInfo[getFieldOffset(Field)] = getNameOf<Field>();
}
std::string_view getFieldName(std::size_t offset) {
if (auto it = fieldInfo.find(offset); it != fieldInfo.end()) {
return it->second;
}
return {};
}
};
struct VariableRuntimeInfo {
std::unordered_map<void *, std::string_view> infos;
std::string_view getVariableName(void *pointer) {
if (auto it = infos.find(pointer); it != infos.end()) {
return it->second;
}
return {};
}
};
template <typename> struct UnwrapFieldInfo;
template <typename T, typename StructT> struct UnwrapFieldInfo<T StructT::*> {
using struct_type = StructT;
using field_type = T;
};
template <typename StructT> auto &getStructStorage() {
static StructRuntimeInfo<StructT> structStorage;
return structStorage;
}
inline auto &getVariableStorage() {
static VariableRuntimeInfo storage;
return storage;
}
template <typename StructT, std::size_t N = fieldCount<StructT>>
constexpr auto getConstStructInfo() {
auto genInfo =
[]<std::size_t... I>(
std::index_sequence<I...>) -> std::array<StructFieldInfo, N> {
std::array<StructFieldQuery, N> queries;
static_cast<void>(StructT{queries[I]...});
auto result = std::array<StructFieldInfo, N>{queries[I].info...};
std::size_t nextOffset = 0;
for (auto &elem : result) {
elem.offset = (nextOffset + (elem.align - 1)) & ~(elem.align - 1);
nextOffset = elem.offset + elem.size;
}
return result;
};
return genInfo(std::make_index_sequence<N>());
}
template <typename StructT> constexpr auto getStructInfo() {
auto structInfo = getConstStructInfo<StructT>();
auto &runtimeInfo = getStructStorage<StructT>();
for (auto &field : structInfo) {
field.name = runtimeInfo.getFieldName(field.offset);
}
return structInfo;
}
} // namespace detail
template <auto Field> void registerField() {
using Info = detail::UnwrapFieldInfo<decltype(Field)>;
auto &storage = detail::getStructStorage<typename Info::struct_type>();
storage.template registerField<Field>();
}
template <auto &&Variable> void registerVariable() {
auto &storage = detail::getVariableStorage();
storage.infos[&Variable] = getNameOf<Variable>();
}
} // namespace rpcsx::ui
template <typename T>
requires(std::is_standard_layout_v<T> && std::is_class_v<T> &&
rpcsx::ui::fieldCount<T> > 0) &&
(!requires(T value) { std::begin(value) != std::end(value); })
struct std::formatter<T> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
std::format_context::iterator format(T &s, std::format_context &ctx) const {
std::format_to(ctx.out(), "{}", rpcsx::ui::getNameOf<T>());
std::format_to(ctx.out(), "{{");
auto structInfo = rpcsx::ui::detail::getStructInfo<T>();
auto bytes = reinterpret_cast<std::byte *>(&s);
for (std::size_t i = 0; i < rpcsx::ui::fieldCount<T>; ++i) {
if (i != 0) {
std::format_to(ctx.out(), ", ");
}
if (!structInfo[i].name.empty()) {
std::format_to(ctx.out(), ".{} = ", structInfo[i].name);
}
structInfo[i].format(bytes + structInfo[i].offset, ctx);
}
std::format_to(ctx.out(), "}}");
return ctx.out();
}
};
template <typename T>
requires(std::is_enum_v<T> && rpcsx::ui::fieldCount<T> > 0)
struct std::formatter<T> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
std::format_context::iterator format(T value,
std::format_context &ctx) const {
auto getFieldName =
[]<std::size_t... I>(std::underlying_type_t<T> value,
std::index_sequence<I...>) -> std::string {
std::string_view result;
((value == I ? ((result = rpcsx::ui::getNameOf<static_cast<T>(I)>()), 0)
: 0),
...);
if (!result.empty()) {
return std::string(result);
}
return std::format("{}", value);
};
auto queryUnknownField =
[]<std::int64_t Offset, std::int64_t... I>(
std::underlying_type_t<T> value,
std::integral_constant<std::int64_t, Offset>,
std::integer_sequence<std::int64_t, I...>) -> std::string {
std::string_view result;
if (value < 0) {
((-value == I + Offset
? ((result =
rpcsx::ui::getNameOf<static_cast<T>(-(I + Offset))>()),
0)
: 0),
...);
} else {
((value == I + Offset
? ((result = rpcsx::ui::getNameOf<static_cast<T>(I + Offset)>()),
0)
: 0),
...);
}
if (!result.empty()) {
return std::string(result);
}
return std::format("{}", value);
};
std::string fieldName;
auto underlying = std::to_underlying(value);
if (underlying < 0) {
fieldName = queryUnknownField(
underlying, std::integral_constant<std::int64_t, 0>{},
std::make_integer_sequence<std::int64_t, 128>{});
} else if (underlying >= rpcsx::ui::fieldCount<T>) {
fieldName = queryUnknownField(
underlying,
std::integral_constant<std::int64_t, rpcsx::ui::fieldCount<T>>{},
std::make_integer_sequence<std::int64_t, 128>{});
} else {
fieldName = getFieldName(
underlying, std::make_index_sequence<rpcsx::ui::fieldCount<T>>());
}
if (fieldName[0] >= '0' && fieldName[0] <= '9') {
std::format_to(ctx.out(), "({}){}", rpcsx::ui::getNameOf<T>(), fieldName);
} else {
std::format_to(ctx.out(), "{}::{}", rpcsx::ui::getNameOf<T>(), fieldName);
}
return ctx.out();
}
};
template <typename T>
requires requires(T value) { std::begin(value) != std::end(value); }
struct std::formatter<T> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
std::format_context::iterator format(T &s, std::format_context &ctx) const {
std::format_to(ctx.out(), "[");
for (bool first = true; auto &elem : s) {
if (first) {
first = false;
} else {
std::format_to(ctx.out(), ", ");
}
std::format_to(ctx.out(), "{}", elem);
}
std::format_to(ctx.out(), "]");
return ctx.out();
}
};
template <typename T>
requires(!std::is_same_v<std::remove_cv_t<T>, char> &&
!std::is_same_v<std::remove_cv_t<T>, wchar_t> &&
!std::is_same_v<std::remove_cv_t<T>, char8_t> &&
!std::is_same_v<std::remove_cv_t<T>, char16_t> &&
!std::is_same_v<std::remove_cv_t<T>, char32_t> &&
std::is_default_constructible_v<std::formatter<T>>)
struct std::formatter<T *> {
constexpr std::format_parse_context::iterator
parse(std::format_parse_context &ctx) {
return ctx.begin();
}
std::format_context::iterator format(T *ptr, std::format_context &ctx) const {
auto name = rpcsx::ui::detail::getVariableStorage().getVariableName(ptr);
if (!name.empty()) {
std::format_to(ctx.out(), "*{} = ", name);
} else {
std::format_to(ctx.out(), "*");
}
if (ptr == nullptr) {
std::format_to(ctx.out(), "nullptr");
} else {
std::format_to(ctx.out(), "{}:{}", static_cast<void *>(ptr), *ptr);
}
return ctx.out();
}
};
#define RX_CONCAT(a, b) RX_CONCAT_IMPL(a, b)
#define RX_CONCAT_IMPL(a, b) a##b
#define RX_REGISTER_VARIABLE(x) \
static auto RX_CONCAT(_RX_REGISTER_VARIABLE_, __LINE__) = ([] { \
::rpcsx::ui::registerVariable<x>(); \
return true; \
}())
#define RX_REGISTER_FIELD(x) \
static auto RX_CONCAT(_RX_REGISTER_FIELD_, __LINE__) = ([] { \
::rpcsx::ui::registerField<&x>(); \
return true; \
}())

View File

@@ -0,0 +1,38 @@
#pragma once
#include "Protocol.hpp"
#include <format>
namespace rpcsx::ui {
template <typename... Args>
void log(LogLevel level, std::format_string<Args...> fmt, Args &&...args) {
Protocol::getDefault()->sendLogMessage(
level, std::vformat(fmt.get(), std::make_format_args(args...)));
}
template <typename... Args>
void ilog(std::format_string<Args...> fmt, Args &&...args) {
Protocol::getDefault()->sendLogMessage(
LogLevel::Info, std::vformat(fmt.get(), std::make_format_args(args...)));
}
template <typename... Args>
void elog(std::format_string<Args...> fmt, Args &&...args) {
Protocol::getDefault()->sendLogMessage(
LogLevel::Error, std::vformat(fmt.get(), std::make_format_args(args...)));
}
template <typename... Args>
void wlog(std::format_string<Args...> fmt, Args &&...args) {
Protocol::getDefault()->sendLogMessage(
LogLevel::Warning,
std::vformat(fmt.get(), std::make_format_args(args...)));
}
template <typename... Args>
[[noreturn]] void fatal(std::format_string<Args...> fmt, Args &&...args) {
Protocol::getDefault()->sendLogMessage(
LogLevel::Fatal, std::vformat(fmt.get(), std::make_format_args(args...)));
std::exit(1);
}
} // namespace rpcsx::ui

View File

@@ -0,0 +1,159 @@
#pragma once
#include <cstddef>
#include <string_view>
#ifdef _MSC_VER
#define RX_PRETTY_FUNCTION __FUNCSIG__
#elif defined(__GNUC__)
#define RX_PRETTY_FUNCTION __PRETTY_FUNCTION__
#else
#define RX_PRETTY_FUNCTION ""
#endif
namespace rpcsx::ui {
namespace detail {
struct AnyStructFieldQuery {
template <typename T> constexpr operator T &&();
};
template <typename StructT, std::size_t N = 1, std::size_t LastValidCount = 0>
requires std::is_class_v<StructT>
constexpr auto calcFieldCount() {
auto isValidFieldCount = []<std::size_t... I>(std::index_sequence<I...>) {
return requires { StructT(((I, AnyStructFieldQuery{}))...); };
};
if constexpr (isValidFieldCount(std::make_index_sequence<N>())) {
return calcFieldCount<StructT, N + 1, N>();
} else if constexpr (sizeof(StructT) <= N || LastValidCount > 0) {
return LastValidCount;
} else {
return calcFieldCount<StructT, N + 1, LastValidCount>();
}
}
consteval std::string_view unwrapName(std::string_view prefix,
std::string_view pretty,
bool dropNamespace) {
#ifdef _MSC_VER
if (auto pos = pretty.find(prefix); pos != std::string_view::npos) {
pretty.remove_prefix(pos + prefix.size());
} else {
pretty = {};
}
if (auto pos = pretty.rfind('>'); pos != std::string_view::npos) {
pretty.remove_suffix(pretty.size() - pos);
} else {
pretty = {};
}
if (auto pos = pretty.rfind(')'); pos != std::string_view::npos) {
pretty.remove_prefix(pos + 1);
}
if (auto pos = pretty.find(' '); pos != std::string_view::npos) {
pretty.remove_prefix(pos + 1);
}
#else
if (auto pos = pretty.rfind('['); pos != std::string_view::npos) {
pretty.remove_prefix(pos + 1);
} else {
pretty = {};
}
if (auto pos = pretty.rfind(prefix); pos != std::string_view::npos) {
pretty.remove_prefix(pos + prefix.size());
} else {
pretty = {};
}
if (auto pos = pretty.rfind(')'); pos != std::string_view::npos) {
pretty.remove_prefix(pos + 1);
}
if (pretty.ends_with(']')) {
pretty.remove_suffix(1);
} else {
pretty = {};
}
#endif
if (dropNamespace) {
if (auto pos = pretty.rfind(':'); pos != std::string_view::npos) {
pretty.remove_prefix(pos + 1);
}
}
return pretty;
}
template <typename> constexpr bool isField = false;
template <typename BaseT, typename TypeT>
constexpr bool isField<TypeT(BaseT::*)> = true;
} // namespace detail
template <auto &&V> consteval auto getNameOf() {
std::string_view prefix;
#ifdef _MSC_VER
prefix = "getNameOf<";
#else
prefix = "V = ";
#endif
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, true);
}
template <auto V>
requires(detail::isField<decltype(V)> ||
std::is_enum_v<std::remove_cvref_t<decltype(V)>> ||
std::is_pointer_v<std::remove_cvref_t<decltype(V)>>)
consteval auto getNameOf() {
std::string_view prefix;
#ifdef _MSC_VER
prefix = "getNameOf<";
#else
prefix = "V = ";
#endif
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, true);
}
template <typename T> consteval auto getNameOf() {
std::string_view prefix;
#ifdef _MSC_VER
prefix = "getNameOf<";
#else
prefix = "T = ";
#endif
return detail::unwrapName(prefix, RX_PRETTY_FUNCTION, false);
}
namespace detail {
template <typename EnumT, std::size_t N = 0>
requires std::is_enum_v<EnumT>
constexpr auto calcFieldCount() {
if constexpr (requires { EnumT::Count; }) {
return static_cast<std::size_t>(EnumT::Count);
} else if constexpr (requires { EnumT::_count; }) {
return static_cast<std::size_t>(EnumT::_count);
} else if constexpr (requires { EnumT::count; }) {
return static_cast<std::size_t>(EnumT::count);
} else if constexpr (!requires { getNameOf<EnumT(N)>()[0]; }) {
return N;
} else {
constexpr auto c = getNameOf<EnumT(N)>()[0];
if constexpr (!requires { getNameOf<EnumT(N)>()[0]; }) {
return N;
} else if constexpr (c >= '0' && c <= '9') {
return N;
} else {
return calcFieldCount<EnumT, N + 1>();
}
}
}
} // namespace detail
template <typename StructT>
inline constexpr std::size_t fieldCount = detail::calcFieldCount<StructT>();
} // namespace rpcsx::ui

View File

@@ -0,0 +1,365 @@
#include "rpcsx/ui/extension.hpp"
#include "rpcsx/ui/Protocol.hpp"
#include "rpcsx/ui/Transport.hpp"
#include <charconv>
#include <cstddef>
#include <cstdio>
#include <exception>
#include <functional>
#include <map>
#include <memory>
#include <nlohmann/json.hpp>
#include <rpcsx-ui.hpp>
#include <string>
#include <string_view>
#include <utility>
using namespace rpcsx::ui;
using namespace nlohmann;
struct StdioTransport : Transport {
void write(std::span<const std::byte> bytes) override {
while (true) {
auto count = std::fwrite(bytes.data(), 1, bytes.size(), stdout);
if (count <= 0) {
break;
}
if (count == bytes.size()) {
break;
}
bytes = bytes.subspan(count);
}
}
void read(std::span<std::byte> &bytes) override {
auto count = std::fread(bytes.data(), 1, bytes.size(), stdin);
bytes = bytes.subspan(0, count);
}
void flush() override { std::fflush(stdout); }
};
template <typename T, typename Protocol>
static auto createMethodHandler(Protocol *protocol) {
return [=](std::size_t id, json params) {
typename T::Request request;
try {
request = params;
} catch (const std::exception &) {
protocol->sendErrorResponse(id, {ErrorCode::InvalidParams});
return;
}
auto result = protocol->getHandlers().handle(request);
if (!result.has_value()) {
protocol->sendErrorResponse(id, result.error());
return;
}
protocol->sendResponse(id, json(result.value()));
};
};
template <typename T, typename Protocol>
static auto createNotifyHandler(Protocol *protocol) {
return [=](json params) {
typename T::Request request;
try {
request = params;
} catch (const std::exception &) {
protocol->sendErrorResponse({ErrorCode::InvalidParams});
return;
}
protocol->getHandlers().handle(request);
};
};
static std::span<const std::byte> asBytes(std::string_view text) {
return {reinterpret_cast<const std::byte *>(text.data()), text.size()};
}
struct JsonRpcProtocol : Protocol {
JsonRpcProtocol(Transport *transport) : Protocol(transport) {
mMethodHandlers["$/initialize"] =
createMethodHandler<rpcsx::ui::Initialize>(this);
mMethodHandlers["$/activate"] =
createMethodHandler<rpcsx::ui::Activate>(this);
// mMethodHandlers["$/deactivate"] =
// createMethodHandler<rpcsx::ui::Deactivate>(this);
mNotifyHandlers["$/shutdown"] =
createNotifyHandler<rpcsx::ui::Shutdown>(this);
}
void call(std::string_view method, json params,
std::function<void(json)> responseHandler) override {
std::size_t id = mNextId++;
send({
{"jsonrpc", "2.0"},
{"method", method},
{"params", std::move(params)},
{"id", id},
});
mExpectedResponses.emplace(id, std::move(responseHandler));
}
void notify(std::string_view method, json params) override {
send({
{"jsonrpc", "2.0"},
{"method", method},
{"params", std::move(params)},
});
}
void sendResponse(std::size_t id, json result) override {
send({
{"jsonrpc", "2.0"},
{"id", id},
{"result", std::move(result)},
});
}
void sendErrorResponse(std::size_t id, ErrorInstance error) override {
send({
{"jsonrpc", "2.0"},
{"id", id},
{"error", error},
});
}
void sendErrorResponse(ErrorInstance error) override {
send({
{"jsonrpc", "2.0"},
{"id", nullptr},
{"error", error},
});
}
void addNotificationHandler(std::string_view notification,
std::function<void(json)> handler) override {
mNotifyHandlers[std::string(notification)] = std::move(handler);
}
void
addMethodHandler(std::string_view method,
std::function<void(std::size_t, json)> handler) override {
mMethodHandlers[std::string(method)] = std::move(handler);
}
void onEvent(std::string_view method,
std::function<void(json)> eventHandler) override {
mEventHandlers[std::string(method)].push_back(std::move(eventHandler));
}
void sendLogMessage(LogLevel level, std::string_view message) override {
// FIXME
std::fprintf(stderr, "%s\n", std::string(message).c_str());
}
int processMessages() override {
std::string header;
std::vector<std::byte> buffer;
while (true) {
header.clear();
while (true) {
std::byte b;
std::span bytes = {&b, 1};
getTransport()->read(bytes);
if (!bytes.empty()) {
header += static_cast<char>(b);
}
if (header.ends_with("\r\n\r\n")) {
break;
}
}
constexpr std::string_view contentLength = "Content-Length:";
auto contentLengthPos = header.find_first_of(contentLength);
if (contentLengthPos == std::string_view::npos) {
continue;
}
std::string_view lengthString = std::string_view(header).substr(
contentLengthPos + contentLength.size());
auto lineEnd = lengthString.find_first_of("\r\n");
if (lineEnd == std::string_view::npos) {
continue;
}
lengthString = lengthString.substr(0, lineEnd);
while (lengthString.starts_with(' ')) {
lengthString.remove_prefix(1);
}
std::size_t length = 0;
auto [ptr, ec] =
std::from_chars(lengthString.data(),
lengthString.data() + lengthString.size(), length);
if (ec != std::errc{} ||
ptr != lengthString.data() + lengthString.size()) {
continue;
}
buffer.resize(length);
std::span bytes = {buffer};
getTransport()->read(bytes);
if (bytes.size() != buffer.size()) {
std::fprintf(stderr, "input truncated\n");
std::abort();
}
std::string_view content = {
(char *)buffer.data(),
buffer.size(),
};
handleRequest(json::parse(content));
}
return 0;
}
void handleRequest(json message) {
auto handlers = getHandlers();
if (auto it = message.find("method"); it != message.end()) {
std::string method = it.value();
std::size_t id = 0;
bool hasId = false;
if (auto it = message.find("id"); it != message.end()) {
hasId = true;
id = it.value();
}
json params;
if (auto it = message.find("params"); it != message.end()) {
params = it.value();
}
if (hasId) {
if (auto it = mMethodHandlers.find(method);
it != mMethodHandlers.end()) {
it->second(id, params);
return;
}
sendErrorResponse(id, {ErrorCode::MethodNotFound});
return;
}
if (auto it = mNotifyHandlers.find(method); it != mNotifyHandlers.end()) {
it->second(params);
return;
}
sendErrorResponse({ErrorCode::MethodNotFound});
return;
}
if (auto it = message.find("result"); it != message.end()) {
json result = it.value();
bool hasId = false;
std::size_t id = 0;
if (auto it = message.find("id"); it != message.end()) {
hasId = true;
id = it.value();
}
if (!hasId) {
return;
}
if (auto it = mExpectedResponses.find(id);
it != mExpectedResponses.end()) {
auto impl = std::move(it->second);
mExpectedResponses.erase(it);
impl(result);
}
}
}
private:
void send(json body) {
std::string bodyText = body.dump();
std::string header = "Content-Length: ";
header += std::to_string(bodyText.length());
header += "\r\n\r\n";
getTransport()->write(asBytes(header));
getTransport()->write(asBytes(bodyText));
getTransport()->flush();
}
std::map<std::string, std::function<void(std::size_t, json)>> mMethodHandlers;
std::map<std::string, std::function<void(json)>> mNotifyHandlers;
std::map<std::string, std::vector<std::function<void(json)>>> mEventHandlers;
std::map<std::size_t, std::function<void(json)>> mExpectedResponses;
std::size_t mNextId = 1;
};
ExtensionBuilder extension_main();
int main(int argc, const char *argv[]) {
auto extensionBuilder = extension_main();
std::string_view transportId;
std::string_view protocolId;
for (int i = 1; i < argc - 1; ++i) {
if (argv[i] == std::string_view("--rpcsx-ui/transport")) {
transportId = argv[i + 1];
++i;
continue;
}
if (argv[i] == std::string_view("--rpcsx-ui/protocol")) {
protocolId = argv[i + 1];
++i;
continue;
}
}
if (transportId.empty()) {
transportId = "stdio";
}
if (protocolId.empty()) {
protocolId = "json-rpc";
}
std::unique_ptr<Transport> transport;
if (transportId == "stdio") {
transport = std::make_unique<StdioTransport>();
} else {
return 1;
}
std::unique_ptr<Protocol> protocol;
if (protocolId == "json-rpc") {
protocol = std::make_unique<JsonRpcProtocol>(transport.get());
} else {
return 1;
}
Protocol::setDefault(protocol.get());
auto exptension = extensionBuilder(protocol.get());
return protocol->processMessages();
}

View File

@@ -0,0 +1,143 @@
#include "rpcsx/ui/file.hpp"
#include <atomic>
#include <cerrno>
#include <cstddef>
#include <cstring>
#if defined(__linux)
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#else
#include <fstream>
#endif
using namespace rpcsx::ui;
#if defined(__linux)
static const std::uint32_t gPageSize = getpagesize();
static int fileNativeHandle(void *impl) {
std::uintptr_t rawHandle = 0;
std::memcpy(&rawHandle, &impl, sizeof(void *));
int fd = rawHandle;
std::atomic<int> a;
a.wait(5);
return -fd;
}
File::~File() {
if (m_impl != nullptr) {
::close(fileNativeHandle(m_impl));
}
}
std::expected<File, std::error_code>
File::open(const std::filesystem::path &path, std::ios::openmode mode) {
int flags = 0;
if (mode & std::ios::out) {
if (mode & std::ios::in) {
flags |= O_RDWR;
} else {
flags |= O_WRONLY;
}
flags |= O_CREAT;
} else {
flags |= O_RDONLY;
}
if (mode & std::ios::trunc) {
flags |= O_TRUNC;
}
if (mode & std::ios::app) {
flags |= O_APPEND;
}
int fd = ::open(path.native().c_str(), flags, 0666);
if (fd < 0) {
return std::unexpected(std::make_error_code(std::errc{errno}));
}
Impl *handle{};
std::uintptr_t rawHandle = -fd;
std::memcpy(&handle, &rawHandle, sizeof(void *));
File result;
result.m_impl = handle;
return result;
}
FileData::~FileData() {
if (data()) {
::munmap(data(), (size() + gPageSize - 1) & ~(gPageSize - 1));
}
}
std::expected<FileData, std::error_code> File::map() {
if (m_impl == nullptr) {
return std::unexpected(std::make_error_code(std::errc::invalid_argument));
}
int fd = fileNativeHandle(m_impl);
struct stat fs;
if (fstat(fd, &fs) < 0) {
return std::unexpected(std::make_error_code(std::errc{errno}));
}
void *mapping = ::mmap(nullptr, fs.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapping == MAP_FAILED) {
return std::unexpected(std::make_error_code(std::errc{errno}));
}
return FileData(nullptr, {(std::byte *)mapping, std::size_t(fs.st_size)});
}
#else
struct File::Impl {
std::fstream stream;
};
struct FileData::Impl {
std::vector<std::byte> data;
};
File::~File() { delete m_impl; }
FileData::~FileData() { delete m_impl; }
std::expected<File, std::error_code>
File::open(const std::filesystem::path &path, std::ios::openmode mode) {
std::fstream f(path, mode);
if (!f.is_open()) {
return std::unexpected(std::make_error_code(std::errc{errno}));
}
File result;
result.m_impl = new Impl();
result.m_impl->stream = std::move(f);
return result;
}
std::expected<FileData, std::error_code> File::map() {
if (m_impl == nullptr) {
return std::unexpected(std::make_error_code(std::errc::invalid_argument));
}
std::size_t pos = m_impl->stream.tellg();
m_impl->stream.seekg(0, std::ios::end);
std::size_t size = m_impl->stream.tellg();
m_impl->stream.seekg(pos, std::ios::beg);
std::vector<std::byte> buffer(size);
m_impl->stream.read(reinterpret_cast<char *>(buffer.data()), buffer.size());
if (!m_impl->stream) {
return std::unexpected(std::make_error_code(std::errc{errno}));
}
auto dataImpl = new FileData::Impl();
return FileData(dataImpl, dataImpl->data);
}
#endif

1023
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,13 @@
},
"scripts": {
"build:kit": "npm run -w rpcsx-ui-kit build",
"build:extensions": "npm run build:api:cpp && cmake -B build-extensions -S extensions/cpp -DCMAKE_EXPORT_COMPILE_COMMANDS=on -DCMAKE_BUILD_TYPE=Release && cmake --build build-extensions --parallel",
"install:extensions": "cp -r build-extensions/bin electron/build/",
"build:api:cpp": "node rpcsx-ui-kit/build/cli.js generate --lang c++ --input rpcsx-ui --output extensions/cpp/rpcsx-ui --name rpcsx-ui",
"build:web:server": "node ./build.mjs",
"build:web:ui": "expo export --platform web --dev --output-dir electron/build/ui --no-minify --source-maps",
"build:web:ui:release": "expo export --platform web --dev --output-dir electron/build/ui",
"build:web": "npm run build:kit && npm run build:web:server && npm run build:web:ui",
"build:web": "npm run build:kit && npm run build:web:server && npm run build:web:ui && npm run build:extensions && npm run install:extensions",
"build:web:release": "npm run build:kit && npm run build:web:server && npm run build:web:ui:release",
"build:android": "npm run build:kit && expo prebuild --platform android && ./android/gradlew assembleDebug -p ./android",
"build:android:release": "npm run build:kit && expo prebuild --platform android && ./android/gradlew assembleRelease -p ./android",
@@ -51,8 +54,6 @@
"expo-system-ui": "~5.0.10",
"expo-web-browser": "~14.2.0",
"glob": "^11.0.3",
"i18next": "^25.3.6",
"i18next-browser-languagedetector": "^8.2.0",
"json5": "^2.2.3",
"monaco-editor": "^0.52.2",
"prettier": "^3.6.2",
@@ -69,9 +70,6 @@
"devDependencies": {
"@expo/metro-config": "~0.20.0",
"@reforged/maker-appimage": "^5.0.0",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@tauri-apps/cli": "^2.8.1",
"@types/node": "^24.3.0",
"@types/react": "~19.0.10",
"electron": "^37.3.1",
@@ -81,9 +79,7 @@
"eslint-config-expo": "~9.2.0",
"eslint-config-prettier": "^10.1.8",
"metro": "^0.82.5",
"postcss": "^8.5.6",
"rpcsx-ui-kit": "file:rpcsx-ui-kit",
"tailwindcss": "^4.1.12",
"tslib": "^2.8.1",
"typescript": "~5.8.3",
"typescript-eslint": "^8.40.0"

View File

@@ -27,7 +27,7 @@ const options = {
format: 'esm',
sourcemap: values.dev ? 'linked' : undefined,
outdir: "build",
entryPoints: ["src/main.ts"]
entryPoints: ["src/main.ts", "src/cli.ts"]
};
try {

View File

@@ -17,14 +17,16 @@
"esbuild": "^0.25.9",
"glob": "^11.0.3",
"json5": "^2.2.3",
"prettier": "^3.6.2"
"prettier": "^3.6.2",
"yargs": "^18.0.0"
},
"devDependencies": {
"@sveltejs/kit": "^2.31.0",
"typescript": "^5.9.2",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.39.1",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8"
"eslint-config-prettier": "^10.1.8",
"typescript": "^5.9.2"
}
}
}

52
rpcsx-ui-kit/src/cli.ts Normal file
View File

@@ -0,0 +1,52 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { ExtensionApiGenerator } from "./cpp-generators.js"
import { ProjectGenerator, RpcsxKit } from "./generators.js"
const libProject: ProjectGenerator = {
name: "libProject",
projectId: "lib",
};
const rendererProject: ProjectGenerator = {
name: "rendererProject",
projectId: "renderer",
};
const serverProject: ProjectGenerator = {
name: "serverProject",
projectId: "server",
};
async function cppGenerator(inDir: string, outDir: string, depLibs: string[], name: string) {
const kit = new RpcsxKit(
[
libProject,
rendererProject,
serverProject,
],
[],
[new ExtensionApiGenerator({ outDir, depLibs, libPrefix: name.split("-") })]
);
await kit.generate([inDir]);
await kit.commit();
}
yargs(hideBin(process.argv))
.strict()
.command("generate", "generate code for language", (yargs) => {
return yargs
.option('lang', {
alias: 'l',
type: 'string',
choices: ["c++"],
demandOption: true
})
.option("input", { type: 'string', normalize: true, description: "Input directory", demandOption: true })
.option("output", { type: 'string', normalize: true, description: "Output directory", demandOption: true })
.option("name", { type: 'string', description: "Name of extension", demandOption: true })
.option("depLibs", { type: 'string', array: true, description: "List of libraries to add as dependencies for generated library" })
}, (argv) => {
cppGenerator(argv.input, argv.output, argv.depLibs ?? [], argv.name);
})
.parseSync();

View File

@@ -0,0 +1,460 @@
import { FileDb, mergeTimestamps } from "./FileDatabase.js";
import {
Component,
ConfigGenerator,
ContributionGenerator,
generateComponentLabelName,
generateContributions,
generateLabelName,
Workspace
} from "./generators.js";
type CmakeGeneratorConfig = {
outDir: string;
libPrefix: string[];
depLibs: string[];
};
function hasContributions(component: Component) {
return component.manifest.contributions != undefined;
}
export class ExtensionApiGenerator implements ConfigGenerator {
constructor(private config: CmakeGeneratorConfig) {
}
private getPrefix(sep: string) {
return this.config.libPrefix.join(sep);
}
async processComponent(component: Component, fileDb: FileDb) {
if (!hasContributions(component)) {
return;
}
const componentPath = `${this.config.outDir}/${component.manifest.name}`;
const includePath = `${componentPath}/include/${this.getPrefix("/")}`;
const typesFile = await fileDb.createFile(`${includePath}/${component.manifest.name}/types.hpp`, component.manifestFile);
const namespace = this.getPrefix("::");
const fullName = this.getPrefix("-");
if (typesFile) {
try {
typesFile.content = generateContributions(component, CppTypesGenerator, namespace, this.getPrefix("/")).toString();
} catch (e) {
throw Error(`${component.manifest.name}: ${e}`);
}
}
const apiFile = await fileDb.createFile(`${includePath}/${component.manifest.name}/api.hpp`, component.manifestFile);
if (apiFile) {
try {
apiFile.content = generateContributions(component, CppApiGenerator, namespace, component.manifest.name).toString();
} catch (e) {
throw Error(`${component.manifest.name}: ${e}`);
}
}
const mainFile = await fileDb.createFile(`${includePath}/${component.manifest.name}.hpp`, component.manifestFile);
if (mainFile) {
mainFile.content = `#pragma once
#include "${component.manifest.name}/types.hpp"
#include "${component.manifest.name}/api.hpp"
`;
}
const cmakeLists = await fileDb.createFile(`${componentPath}/CMakeLists.txt`, component.manifestFile);
if (cmakeLists) {
cmakeLists.content = `
add_library(${fullName}-${component.manifest.name} INTERFACE)
add_library(${namespace}::${component.manifest.name} ALIAS ${fullName}-${component.manifest.name})
target_include_directories(${fullName}-${component.manifest.name} INTERFACE include)
target_link_libraries(${fullName}-${component.manifest.name} INTERFACE
${component.dependencies.filter(x => hasContributions(x)).map(x => ` ${namespace}::${x.manifest.name}\n`).join("")}
${this.config.depLibs.join("\n")}
)
`;
}
}
async processWorkspace(workspace: Workspace, fileDb: FileDb) {
const components = Object.values(workspace).filter(p => hasContributions(p));
const timestamp = mergeTimestamps(components.map(x => x.manifestFile));
const cmakeLists = await fileDb.createFile(
`${this.config.outDir}/CMakeLists.txt`,
timestamp
);
if (cmakeLists) {
cmakeLists.content = `
${components.map(x => `add_subdirectory(${x.manifest.name})\n`).join("")}
add_library(${this.getPrefix("-")} INTERFACE)
add_library(${this.getPrefix("::")} ALIAS ${this.getPrefix("-")})
target_include_directories(${this.getPrefix("-")} INTERFACE include)
target_link_libraries(${this.getPrefix("-")} INTERFACE
${components.map(x => ` ${this.getPrefix("::")}::${x.manifest.name}\n`).join("")}
)
`;
}
const interfaceFile = await fileDb.createFile(
`${this.config.outDir}/include/${this.getPrefix("-")}.hpp`,
timestamp
);
if (interfaceFile) {
interfaceFile.content = `#pragma once
${components.map(x => `#include <${this.getPrefix("/")}/${x.manifest.name}.hpp>`).join("\n")}
`
}
}
};
class CppTypesGenerator implements ContributionGenerator {
generatedTypes: Record<string, string> = {};
includes = new Set<string>();
constructor(private namespace: string, private libPath: string) {
}
toString() {
let result = '#pragma once\n\n';
result += [...this.includes].map(x => `#include <${x}>`).join("\n");
result += `\n\nnamespace ${this.namespace} {\n`;
result += Object.values(this.generatedTypes).join("\n");
return result + `\n} // namespace ${this.namespace}\n`;
}
generateType(component: string, type: object, name: string) {
const labelName = generateComponentLabelName(component, name, true);
const typeName = labelName;
if (typeof type != 'object') {
throw `${type}: must be object`;
}
if (!("type" in type)) {
throw `${type}: type must be present`;
}
if (typeof type.type != "string") {
throw `${name}: type must be string value`;
}
if (!(typeName in this.generatedTypes)) {
let paramsType = "";
if (type.type === "object") {
if (!("params" in type)) {
throw `${type}: params must be present`;
}
if ((typeof type.params != 'object') || !type.params) {
throw `${type.params}: must be object`;
}
paramsType += `struct ${labelName} {\n${this.generateObjectBody(component, type.params)}};\n\n`;
paramsType += this.generateObjectSerializer(labelName, type.params);
paramsType += this.generateObjectDeserializer(labelName, type.params);
} else if (type.type === "enum") {
if (!("enumerators" in type)) {
throw `${type}: enumerators must be present`;
}
if ((typeof type.enumerators != 'object') || !type.enumerators) {
throw `${type.enumerators}: must be object`;
}
paramsType += `enum class ${labelName} {\n${this.generateEnumBody(type.enumerators)}};\n`;
}
this.generatedTypes[typeName] = paramsType;
} else {
throw new Error(`${name}: type ${typeName} already declared`);
}
}
generateEnumBody(enumerators: object) {
let body = "";
Object.keys(enumerators).forEach(fieldName => {
const value = (enumerators as Record<string, object>)[fieldName];
body += ` ${generateLabelName(fieldName, true)} = ${value},\n`;
});
return body;
}
generateObjectBody(component: string, params: object) {
let body = "";
Object.keys(params).forEach(fieldName => {
const param = (params as Record<string, object>)[fieldName];
if (typeof param != 'object') {
throw `${fieldName}: must be object`;
}
if (!("type" in param)) {
throw `${fieldName}: type must be present`;
}
if (typeof param.type != "string") {
throw `${fieldName}: type must be string value`;
}
const isOptional = ("optional" in param) && param.optional === true;
const fieldLabel = generateLabelName(fieldName, false);
let fieldType = this.getTypeName(component, param.type, param);
if (isOptional) {
this.addInclude("optional");
fieldType = `std::optional<${fieldType}>`;
}
body += ` ${fieldType} ${fieldLabel};\n`;
});
return body;
}
generateMethod(component: string, method: object, name: string) {
const labelName = generateComponentLabelName(component, name, true);
const requestTypeName = `${labelName}Request`;
const responseTypeName = `${labelName}Response`;
if (!(requestTypeName in this.generatedTypes)) {
let paramsType = '';
paramsType += `struct ${requestTypeName} {\n`;
if ("params" in method && method.params && typeof method.params == "object") {
paramsType += this.generateObjectBody(component, method.params);
}
paramsType += "};\n";
paramsType += this.generateObjectSerializer(requestTypeName, "params" in method ? method.params ?? {} : {});
paramsType += this.generateObjectDeserializer(requestTypeName, "params" in method ? method.params ?? {} : {});
this.generatedTypes[requestTypeName] = paramsType;
} else {
throw new Error(`${name}: type ${requestTypeName} already declared`);
}
if (!(responseTypeName in this.generatedTypes)) {
let responseType = `struct ${responseTypeName} {\n`;
if ("returns" in method && method.returns && typeof method.returns == "object") {
responseType += this.generateObjectBody(component, method.returns);
}
responseType += "};\n";
responseType += this.generateObjectSerializer(responseTypeName, "returns" in method ? method.returns ?? {} : {});
responseType += this.generateObjectDeserializer(responseTypeName, "returns" in method ? method.returns ?? {} : {});
this.generatedTypes[responseTypeName] = responseType;
} else {
throw new Error(`${name}: type ${responseTypeName} already declared`);
}
this.generatedTypes[labelName] = `struct ${labelName} {
using Request = ${requestTypeName};
using Response = ${responseTypeName};
};
`
}
generateNotification(component: string, notification: object, name: string) {
const labelName = generateComponentLabelName(component, name, true);
const requestTypeName = `${labelName}Request`;
if (!(requestTypeName in this.generatedTypes)) {
let paramsType = `struct ${requestTypeName} {\n`;
if ("params" in notification && notification.params && typeof notification.params == "object") {
paramsType += this.generateObjectBody(component, notification.params);
}
paramsType += "};\n";
paramsType += this.generateObjectSerializer(requestTypeName, "params" in notification ? notification.params ?? {} : {});
paramsType += this.generateObjectDeserializer(requestTypeName, "params" in notification ? notification.params ?? {} : {});
this.generatedTypes[requestTypeName] = paramsType;
} else {
throw new Error(`${name}: type ${requestTypeName} already declared`);
}
this.generatedTypes[labelName] = `struct ${labelName} {
using Request = ${requestTypeName};
};
`
}
generateEvent(component: string, event: object, name: string) {
const labelName = generateComponentLabelName(component, name, true);
const typeName = `${labelName}Event`;
if (!(typeName in this.generatedTypes)) {
if (typeof event == 'object') {
let paramsType = `struct ${typeName} {\n`;
paramsType += this.generateObjectBody(component, event);
paramsType += "\n};\n"
paramsType += this.generateObjectSerializer(typeName, event);
paramsType += this.generateObjectDeserializer(typeName, event);
this.generatedTypes[typeName] = paramsType;
} else if (typeof event == 'string') {
this.generatedTypes[typeName] = `using ${typeName} = ${this.getTypeName(component, event)};\n`;
} else {
throw new Error(`${name}: must be object or string`);
}
} else {
throw new Error(`${name}: type ${typeName} already declared`);
}
}
addInclude(include: string) {
this.includes.add(include);
}
getTypeName(component: string, type: string, object?: object): string {
switch (type) {
case "string":
this.addInclude("string")
return "std::string";
case "number":
return "int";
case "void":
return "void";
case "boolean":
return "bool";
case "json":
this.addInclude("nlohmann/json.hpp");
return "nlohmann::json";
case "json-object":
return "nlohmann::json::object_t";
case "json-array":
return "nlohmann::json::array_t";
case "array":
if (!object || !("item-type" in object) || typeof object["item-type"] != "string") {
throw new Error(`item-type must be defined for array`);
}
this.addInclude("vector");
return `std::vector<${this.getTypeName(component, object["item-type"])}>`;
default:
if (type.startsWith("$")) {
const [refComponent, ...nameParts] = type.split("/");
const typeName = nameParts.join("/");
const refComponentName = refComponent.slice(1);
if (refComponentName != component) {
this.addInclude(`${this.libPath}/${refComponentName}.hpp`);
}
return generateComponentLabelName(refComponentName, typeName, true);
}
return generateComponentLabelName(component, type, true);
}
}
generateObjectSerializer(typename: string, body: object): string {
this.addInclude("nlohmann/json.hpp");
return `inline void to_json(nlohmann::json &json, const ${typename} &value) {
json = nlohmann::json::object_t{
${Object.keys(body).map(field => ` { "${generateLabelName(field, false)}", value.${generateLabelName(field, false)} }`).join(",\n")}
};
}\n\n`;
}
generateObjectDeserializer(typename: string, body: object): string {
this.addInclude("nlohmann/json.hpp");
return `inline void from_json(const nlohmann::json &json, ${typename} &value) {
${Object.keys(body).map(field => {
const label = generateLabelName(field, false);
const fieldObject = (body as any)[field];
const isOptional = ("optional" in fieldObject) && fieldObject.optional === true;
if (!isOptional) {
return ` json.at("${label}").get_to(value.${label});`;
}
return ` if (json.contains("${label}")) {
value.${label} = json["${label}"];
} else {
value.${label} = std::nullopt;
}`;
}).join("\n")}
}\n`;
}
};
class CppApiGenerator implements ContributionGenerator {
private content = '';
constructor(private namespace: string, private componentName: string) {
}
toString() {
const label = generateLabelName(this.componentName, true);
return `#pragma once
#include "./types.hpp"
#include <functional>
#include <utility>
namespace ${this.namespace} {
template <typename InstanceT> class ${label}Interface {
private:
auto &protocol() { return static_cast<InstanceT *>(this)->getProtocol(); }
public:${this.content}
};
struct ${label} {
template <typename InstanceT>
using instance = ${label}Interface<InstanceT>;
static constexpr auto name = "${this.componentName}";
};
} // namespace ${this.namespace}
`;
}
generateMethod(component: string, method: object, name: string) {
const uLabel = generateComponentLabelName(component, name, true);
const label = generateComponentLabelName(component, name, false);
const returnType = "returns" in method ? `${uLabel}Response` : '';
const params = "params" in method ? `const ${uLabel}Request &params, ` : '';
this.content += `
auto ${label}(${params}std::function<void(${returnType})> result) {
return protocol().call("${name}", ${params ? "params" : "{}"}, std::move(result));
}`
}
generateNotification(component: string, notification: object, name: string) {
const uLabel = generateComponentLabelName(component, name, true);
const label = generateComponentLabelName(component, name, false);
const params = "params" in notification ? `const ${uLabel}Request &params` : '';
this.content += `
void ${label}(${params}) {
protocol().notify("${name}", ${params ? "params" : "{}"});
}`
}
generateEvent(component: string, event: object, name: string) {
const uLabel = generateComponentLabelName(component, name, true);
let typeName = '';
if (typeof event == 'object') {
typeName = Object.keys(event).length > 0 ? `${uLabel}Event` : '';
} else if (typeof event == 'string') {
typeName = `${uLabel}Event`;
} else {
throw new Error(`${name}: must be object or string`);
}
this.content += `
auto on${uLabel}(std::function<void(${typeName})> callback) {
return protocol().onEvent("${name}", std::move(callback));
}`
}
};

View File

@@ -97,7 +97,7 @@ function pascalToCamelCase(name: string) {
return name[0].toLowerCase() + name.slice(1);
}
function generateLabelName(entityName: string, isPascalCase = false) {
export function generateLabelName(entityName: string, isPascalCase = false) {
const name = entityName.replaceAll(" ", "-").replaceAll("_", "-").replaceAll(".", "-").replaceAll("/", "-").split("-");
return [...(isPascalCase ? name[0][0].toUpperCase() + name[0].slice(1).toLowerCase() : name[0].toLowerCase()), ...name.slice(1).map(word => {
if (word.length == 0) {
@@ -107,7 +107,7 @@ function generateLabelName(entityName: string, isPascalCase = false) {
})].reduce((a, b) => a + b);
}
function generateComponentLabelName(componentName: string, entityName: string, isPascalCase = false) {
export function generateComponentLabelName(componentName: string, entityName: string, isPascalCase = false) {
return generateLabelName(componentName == 'core' ? entityName : `${componentName}/${entityName}`, isPascalCase);
}
@@ -869,7 +869,7 @@ export function set${name}View(target: Window, params: ${name}Props) {
return `${generatedHeader}
import { thisComponent } from "$/component-info";
import * as ${generateLabelName(this.externalComponent, false)} from '$${this.externalComponent}/api';
${this.viewBody && "import { Window } from '$core/Window';" }
${this.viewBody && "import { Window } from '$core/Window';"}
${this.body}
${this.viewBody}
@@ -997,7 +997,7 @@ export function set${name}View(target: Window, params: ${name}Props) {
import { createError } from "$core/Error";
import { Component } from "$core/Component";
import * as core from "$core";
${this.viewBody && "import { Window } from '$core/Window';" }
${this.viewBody && "import { Window } from '$core/Window';"}
export async function call(_caller: Component, _method: string, _params: JsonObject | undefined): Promise<JsonObject | void> {
throw createError(ErrorCode.MethodNotFound);
@@ -1018,7 +1018,7 @@ import { createError } from "$core/Error";
import { Component } from "$core/Component";
import { thisComponent } from "$/component-info";
import * as core from "$core";
${this.viewBody && "import { Window } from '$core/Window';" }
${this.viewBody && "import { Window } from '$core/Window';"}
export { thisComponent } from "$/component-info";
${this.body}
@@ -1135,7 +1135,7 @@ export function popView() {
}
};
function generateContributions<Params extends [], RT extends ContributionGenerator>(component: Component, Generator: new (...params: Params) => RT, ...params: Params) {
export function generateContributions<Params extends any[], RT extends ContributionGenerator>(component: Component, Generator: new (...params: Params) => RT, ...params: Params) {
const generator = new Generator(...params);
const contributions = component.manifest.contributions ?? {};
@@ -1345,7 +1345,7 @@ export class TsServerGenerator implements ProjectGenerator {
return ["lib"].includes(projectId);
}
async generateContributionFile<Params extends []>(sourceComponent: Component, project: Project, fileDb: FileDb, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
async generateContributionFile<Params extends any[]>(sourceComponent: Component, project: Project, fileDb: FileDb, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
const projectPath = path.join(this.config.outDir, project.component.manifest.name, project.name);
const genDir = path.join(projectPath, "src");
const generatedFilePath = path.join(genDir, generatedFileName);
@@ -1360,7 +1360,7 @@ export class TsServerGenerator implements ProjectGenerator {
}
}
async generateProject<Params extends []>(project: Project, fileDb: FileDb, projectName: string, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
async generateProject<Params extends any[]>(project: Project, fileDb: FileDb, projectName: string, generatedFileName: string, Generator: new (...params: Params) => ContributionGenerator, ...params: Params) {
const newProjectPath = path.join(this.config.outDir, project.component.manifest.name, projectName);
const genDir = path.join(newProjectPath, "src");
const generatedFilePath = path.join(genDir, generatedFileName);

View File

@@ -269,8 +269,7 @@
}
},
"shutdown": {
"handler": "shutdown",
"params": {}
"handler": "shutdown"
},
"component/activate": {
"handler": "activateComponent",

View File

@@ -4,4 +4,4 @@ export const builtinResourcesPath = import.meta.dirname;
export const rootPath = path.dirname(process.execPath);
export const configPath = rootPath;
export const extensionsPath = path.join(rootPath, "extensions");
export const localExtensionsPath = path.join(extensionsPath, ".local");
export const localExtensionsPath = path.join(builtinResourcesPath, "extensions");

View File

@@ -63,19 +63,26 @@ export class ComponentInstance implements ComponentContext {
}
async activate(settings?: JsonObject) {
if (!settings) {
const schema = this.getContribution("settings");
if (schema) {
settings = settingsGet(this.getName(), schema as Record<string, Schema>);
this.activated = true;
try {
if (!settings) {
const schema = this.getContribution("settings");
if (schema) {
settings = settingsGet(this.getName(), schema as Record<string, Schema>);
}
settings ??= {};
}
settings ??= {};
onComponentActivation(this);
await this.impl.activate(this, settings);
} catch (e) {
this.activated = false;
throw e;
}
await this.impl.activate(this, settings);
onComponentActivation(this);
this.emitEvent(activateEvent);
this.activated = true;
}
async deactivate() {

View File

@@ -77,7 +77,7 @@ export class Extension implements IComponentImpl {
if (line.length == 0) {
continue;
}
process.stderr.write(`${date} [${this.manifest.name}-v${this.manifest.version}] ${line}\n`);
process.stderr.write(`${date} [${this.manifest.name[0].text}-v${this.manifest.version}] ${line}\n`);
}
}

View File

@@ -11,7 +11,7 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise<Exte
return;
}
const extensionManifestLocation = path.join(locations.extensionsPath, request.id, "extension.json");
const extensionManifestLocation = path.join(locations.localExtensionsPath, request.id, "extension.json");
const manifestText = await (async () => {
try {
@@ -36,7 +36,7 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise<Exte
const process = await (async () => {
try {
return launcher.launch(path.join(locations.extensionsPath, request.id, manifest.executable), manifest.args ?? [], {
return launcher.launch(path.join(locations.localExtensionsPath, request.id, manifest.executable), manifest.args ?? [], {
launcherRequirements: manifest.launcher.requirements ?? {},
});
} catch {

View File

@@ -59,7 +59,7 @@
"optional": true
},
"launcher": {
"type": "launcher-info",
"type": "$core/launcher-info",
"optional": true
},
"title-id": {
@@ -113,7 +113,7 @@
"optional": true
},
"launcher": {
"type": "launcher-info",
"type": "$core/launcher-info",
"optional": true
},
"title-id": {

View File

@@ -0,0 +1,2 @@
export function activateLocalExtensions(_list: Set<string>) {
}

View File

@@ -0,0 +1,43 @@
import * as core from "$core";
import * as locations from "$core/locations";
import fs from 'fs/promises';
import path from 'path';
export async function activateLocalExtensions(list: Set<string>) {
try {
for (const entry of await fs.readdir(locations.localExtensionsPath, { withFileTypes: true, encoding: 'utf-8' })) {
if (!entry.isDirectory()) {
continue;
}
try {
await fs.stat(path.join(path.join(entry.parentPath, entry.name, "extension.json")));
} catch {
continue;
}
try {
await core.extensionLoad({ id: entry.name });
} catch (e) {
console.error(`failed to load local extension ${entry.name}`, e);
continue;
}
try {
await core.componentActivate({ id: entry.name });
} catch (e) {
console.error(`failed to activate extension ${entry.name}`, e);
try {
await core.extensionUnload({ id: entry.name });
} catch (e) {
console.error(`failed to unload extension ${entry.name}`, e);
}
continue;
}
list.add(entry.name);
}
} catch (e) {
console.error(`failed to load local extensions`, e);
}
}

View File

@@ -1,3 +1,17 @@
import { activateLocalExtensions } from './extension-host';
import * as core from "$core";
const activatedExtensions = new Set<string>();
export function activate() {
return activateLocalExtensions(activatedExtensions);
}
export async function deactivate() {
for (const extension of activatedExtensions) {
core.componentDeactivate({
id: extension
});
}
}

View File

@@ -1,7 +1,6 @@
{
"name": "github",
"version": "0.1.0",
"contributions": {
"settings": {
"url": {
@@ -10,6 +9,17 @@
}
},
"types": {
"asset": {
"type": "object",
"params": {
"name": {
"type": "string"
},
"browser_download_url": {
"type": "string"
}
}
},
"release": {
"type": "object",
"params": {
@@ -21,17 +31,6 @@
"item-type": "asset"
}
}
},
"asset": {
"type": "object",
"params": {
"name": {
"type": "string"
},
"browser_download_url": {
"type": "string"
}
}
}
},
"methods": {