Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/windows-native-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ auto id = api.increment_async(
Shared State has the same generated shape, such as
`get_counter_async(...)` and `set_counter_async(value, ...)`.

### Polling the backend

Every RPC also gains a bounded-wait companion, `name_within(arguments,
std::chrono::milliseconds)`. Its completion resolves a promise, so the call
waits up to the timeout and **cancels the pending request on a miss**; Void
RPCs return `bool` (false = timed out), other results return
`std::optional<T>`. Backend and decoding errors still throw from the final
get, matching the blocking call's contract.

```cpp
// A 400 ms UI tick polls without spawning a thread and without blocking
// past the frame budget:
if (auto value = api.increment_within(41, std::chrono::milliseconds{0})) {
counter_label().Text(std::to_wstring(*value));
}
```

This is the sanctioned shape for long-lived polling UIs. Do not spawn a
detached `std::thread` per tick: each one races the reader thread, leaks
past teardown, and churns a native thread per frame. Prefer completions
when an update push model works; use `_within` when the UI owns a tick
loop, with a zero timeout for a pure poll or a small budget when a
bounded wait is acceptable.

### Threading

RVT1 responses are decoded on Rivet's native reader thread. Normal completion
Expand Down
63 changes: 63 additions & 0 deletions platform/linux/host/GeneratedBackend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// compiles before the first build.
#pragma once

#include <chrono>
#include <cstdint>
#include <exception>
#include <functional>
Expand Down Expand Up @@ -67,6 +68,52 @@ class API {
});
}

// Bounded-wait companions: the completion resolves a promise, so a
// poller waits up to the timeout and cancels the pending request on a
// miss. Poll a long-lived UI from one call site instead of spawning a
// detached thread per tick.
std::optional<std::string> greet_within(
std::string name, std::chrono::milliseconds rivet_timeout) {
auto rivet_done = std::make_shared<std::promise<std::string>>();
auto rivet_id = backend_.request_async(
"greet", rivet::linux_runtime::Value::List{rivet::Value(std::move(name))},
[rivet_done](rivet::linux_runtime::CallResult call) mutable {
auto result = unbox_string(std::move(call));
if (result.error) {
rivet_done->set_exception(result.error);
} else {
rivet_done->set_value(*result.value);
}
});
if (rivet_done->get_future().wait_for(rivet_timeout) !=
std::future_status::ready) {
backend_.cancel(rivet_id);
return std::nullopt;
}
return rivet_done->get_future().get();
}

std::optional<std::int64_t> increment_within(
std::int64_t value, std::chrono::milliseconds rivet_timeout) {
auto rivet_done = std::make_shared<std::promise<std::int64_t>>();
auto rivet_id = backend_.request_async(
"increment", rivet::linux_runtime::Value::List{rivet::Value(value)},
[rivet_done](rivet::linux_runtime::CallResult call) mutable {
auto result = unbox_int64(std::move(call));
if (result.error) {
rivet_done->set_exception(result.error);
} else {
rivet_done->set_value(*result.value);
}
});
if (rivet_done->get_future().wait_for(rivet_timeout) !=
std::future_status::ready) {
backend_.cancel(rivet_id);
return std::nullopt;
}
return rivet_done->get_future().get();
}

// State accessors: completion callbacks run on the backend reader thread
// and must dispatch to the UI thread before touching widgets.
void get_counter_async(
Expand All @@ -89,6 +136,22 @@ class API {
}

private:
static rivet_app::Result<std::string> unbox_string(rivet::linux_runtime::CallResult call) {
rivet_app::Result<std::string> result;
if (!call.succeeded()) {
result.error = call.error ? call.error :
std::make_exception_ptr(std::runtime_error("unknown backend failure"));
return result;
}
if (auto text = std::get_if<std::string>(&call.value->data)) {
result.value = *text;
return result;
}
result.error = std::make_exception_ptr(
std::runtime_error("Rivet result type mismatch: String"));
return result;
}

static rivet_app::Result<std::int64_t> unbox_int64(rivet::linux_runtime::CallResult call) {
rivet_app::Result<std::int64_t> result;
if (!call.succeeded()) {
Expand Down
63 changes: 63 additions & 0 deletions platform/windows/host/GeneratedBackend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// compiles before the first build, mirroring the Linux scaffold header.
#pragma once

#include <chrono>
#include <cstdint>
#include <exception>
#include <functional>
Expand Down Expand Up @@ -67,6 +68,52 @@ class API {
});
}

// Bounded-wait companions: the completion resolves a promise, so a
// poller waits up to the timeout and cancels the pending request on a
// miss. Poll a long-lived UI from one call site instead of spawning a
// detached thread per tick.
std::optional<std::string> greet_within(
std::string name, std::chrono::milliseconds rivet_timeout) {
auto rivet_done = std::make_shared<std::promise<std::string>>();
auto rivet_id = backend_.request_async(
"greet", rivet::windows::Value::List{rivet::Value(std::move(name))},
[rivet_done](rivet::windows::CallResult call) mutable {
auto result = unbox_string(std::move(call));
if (result.error) {
rivet_done->set_exception(result.error);
} else {
rivet_done->set_value(*result.value);
}
});
if (rivet_done->get_future().wait_for(rivet_timeout) !=
std::future_status::ready) {
backend_.cancel(rivet_id);
return std::nullopt;
}
return rivet_done->get_future().get();
}

std::optional<std::int64_t> increment_within(
std::int64_t value, std::chrono::milliseconds rivet_timeout) {
auto rivet_done = std::make_shared<std::promise<std::int64_t>>();
auto rivet_id = backend_.request_async(
"increment", rivet::windows::Value::List{rivet::Value(value)},
[rivet_done](rivet::windows::CallResult call) mutable {
auto result = unbox_int64(std::move(call));
if (result.error) {
rivet_done->set_exception(result.error);
} else {
rivet_done->set_value(*result.value);
}
});
if (rivet_done->get_future().wait_for(rivet_timeout) !=
std::future_status::ready) {
backend_.cancel(rivet_id);
return std::nullopt;
}
return rivet_done->get_future().get();
}

// State accessors: completion callbacks run on the backend reader thread
// and must dispatch to the UI thread before touching widgets.
void get_counter_async(
Expand All @@ -89,6 +136,22 @@ class API {
}

private:
static rivet_app::Result<std::string> unbox_string(rivet::windows::CallResult call) {
rivet_app::Result<std::string> result;
if (!call.succeeded()) {
result.error = call.error ? call.error :
std::make_exception_ptr(std::runtime_error("unknown backend failure"));
return result;
}
if (auto text = std::get_if<std::string>(&call.value->data)) {
result.value = *text;
return result;
}
result.error = std::make_exception_ptr(
std::runtime_error("Rivet result type mismatch: String"));
return result;
}

static rivet_app::Result<std::int64_t> unbox_int64(rivet::windows::CallResult call) {
rivet_app::Result<std::int64_t> result;
if (!call.succeeded()) {
Expand Down
63 changes: 60 additions & 3 deletions rivet-cli/codegen/cpp.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@

(define (cpp-async-handler result-type backend-namespace)
(define result (cpp-type result-type))
;; Result<void> has no value member, so the Void decode must not assign
;; into it — it only validates that a value arrived.
(define decode
(if (eq? result-type 'Void)
(format "detail::decode_~a(*rivet_result.value);" (type-key result-type))
Expand All @@ -148,6 +150,60 @@
"[completion = std::move(completion)](~a::CallResult rivet_result) mutable { Result<~a> result; if (rivet_result.error) { result.error = rivet_result.error; } else { try { if (!rivet_result.value) throw std::runtime_error(\"Rivet async call completed without a value\"); ~a } catch (...) { result.error = std::current_exception(); } } completion(std::move(result)); }"
backend-namespace result decode))

;; Bounded-wait companion (issue #252): the completion side of request_async
;; resolves a promise, so a poller waits up to the timeout and cancels the
;; request if it is still pending. Void RPCs return bool (false = timed
;; out); other results return std::nullopt on timeout. Backend errors still
;; throw from the final get, keeping the blocking call's error contract.
(define (cpp-promise-handler result-type backend-namespace)
(define settle
(if (eq? result-type 'Void)
(format "detail::decode_~a(*rivet_result.value);" (type-key result-type))
(format "rivet_done->set_value(detail::decode_~a(*rivet_result.value));"
(type-key result-type))))
(format
"[rivet_done](~a::CallResult rivet_result) mutable { if (rivet_result.error) { rivet_done->set_exception(rivet_result.error); } else { try { if (!rivet_result.value) throw std::runtime_error(\"Rivet async call completed without a value\"); ~a } catch (...) { rivet_done->set_exception(std::current_exception()); } } }"
backend-namespace settle))

(define (cpp-within-method info backend-namespace)
(define names (map cpp-id (schema-rpc-arg-names info)))
(define types (schema-rpc-arg-types info))
(define result-type (schema-rpc-result-type info))
(define void? (eq? result-type 'Void))
(define params
(string-join
(for/list ([name (in-list names)] [type (in-list types)])
(format "~a ~a" (cpp-type type) name))
", "))
(define encoded
(string-join
(for/list ([name (in-list names)] [type (in-list types)])
(format "detail::encode_~a(~a)" (type-key type) name))
", "))
(define rpc-name (cpp-string-literal (symbol->string (schema-rpc-name info))))
(define return-type
(if void? "bool" (format "std::optional<~a>" (cpp-type result-type))))
(define timed-out-return (if void? "false" "std::nullopt"))
(define final-get
(if void?
"rivet_done->get_future().get(); return true;"
"return rivet_done->get_future().get();"))
;; A zero-argument RPC has no parameter list; the timeout must not follow
;; a stray comma.
(define within-params
(if (string=? params "")
"std::chrono::milliseconds rivet_timeout"
(format "~a, std::chrono::milliseconds rivet_timeout" params)))
(format
" ~a ~a_within(~a) { auto rivet_done = std::make_shared<std::promise<~a>>(); auto rivet_id = backend_.request_async(~a, rivet::Value::List{~a}, ~a); if (rivet_done->get_future().wait_for(rivet_timeout) != std::future_status::ready) { backend_.cancel(rivet_id); return ~a; } ~a }\n"
return-type
(cpp-id (schema-rpc-name info))
within-params
(cpp-type result-type)
rpc-name encoded
(cpp-promise-handler result-type backend-namespace)
timed-out-return final-get))

(define (cpp-completion-type type)
(format "std::function<void(Result<~a>)>" (cpp-type type)))

Expand All @@ -173,7 +229,7 @@
(cpp-append-param params
(format "~a completion" (cpp-completion-type result-type))))
(format
" std::future<~a> ~a(~a) { ~a }\n [[nodiscard]] std::uint64_t ~a_async(~a) { if (!completion) throw std::invalid_argument(\"Rivet async completion handler is empty\"); return backend_.request_async(~a, rivet::Value::List{~a}, ~a); }\n"
" std::future<~a> ~a(~a) { ~a }\n [[nodiscard]] std::uint64_t ~a_async(~a) { if (!completion) throw std::invalid_argument(\"Rivet async completion handler is empty\"); return backend_.request_async(~a, rivet::Value::List{~a}, ~a); }\n~a"
(cpp-type result-type)
(cpp-id (schema-rpc-name info))
params
Expand All @@ -182,7 +238,8 @@
rpc-name encoded))
(cpp-id (schema-rpc-name info))
async-params
rpc-name encoded (cpp-async-handler result-type backend-namespace)))
rpc-name encoded (cpp-async-handler result-type backend-namespace)
(cpp-within-method info backend-namespace)))

(define (cpp-state-methods state backend-namespace)
(define name (cpp-id (schema-state-name state)))
Expand Down Expand Up @@ -228,7 +285,7 @@
identifier release-channel backend-namespace)
(define types (all-types rpcs events states records enums))
(string-append
"// Generated by Rivet. Do not edit by hand.\n#pragma once\n\n#include <cstdint>\n#include <exception>\n#include <functional>\n#include <future>\n#include <optional>\n#include <stdexcept>\n#include <string>\n#include <utility>\n#include <variant>\n#include <vector>\n\n#include \"backend.hpp\"\n\nnamespace rivet_app {\n"
"// Generated by Rivet. Do not edit by hand.\n#pragma once\n\n#include <chrono>\n#include <cstdint>\n#include <exception>\n#include <functional>\n#include <future>\n#include <optional>\n#include <stdexcept>\n#include <string>\n#include <utility>\n#include <variant>\n#include <vector>\n\n#include \"backend.hpp\"\n\nnamespace rivet_app {\n"
(format "inline constexpr char kModuleName[] = ~a;\ninline constexpr char kEntryName[] = ~a;\ninline constexpr char kDisplayName[] = ~a;\ninline constexpr char kVersion[] = ~a;\ninline constexpr std::int64_t kBuild = ~a;\ninline constexpr char kIdentifier[] = ~a;\ninline constexpr char kReleaseChannel[] = ~a;\n\n"
(cpp-string-literal module-name)
(cpp-string-literal entry-name)
Expand Down
4 changes: 3 additions & 1 deletion rivet-cli/codegen/naming.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@
(define name (cpp-id (schema-rpc-name info)))
(list (cons name (format "RPC ~a" (schema-rpc-name info)))
(cons (string-append name "_async")
(format "RPC async companion ~a" (schema-rpc-name info))))))
(format "RPC async companion ~a" (schema-rpc-name info)))
(cons (string-append name "_within")
(format "RPC bounded-wait companion ~a" (schema-rpc-name info))))))
(append*
(for/list ([state (in-list states)])
(define name (cpp-id (schema-state-name state)))
Expand Down
36 changes: 33 additions & 3 deletions rivet/tests/codegen.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,17 @@
(define-rpc (echo-user [user : User] : User)
user)

(define-rpc (log-line [message String] : Void)
(displayln message))

(define (start in-fd out-fd)
(serve-fds in-fd out-fd))
RKT
)

(define project (load-project project-root))
(define schema (generate-clients! project))
(check-equal? (length (first schema)) 3)
(check-equal? (length (first schema)) 4)
(check-equal? (length (second schema)) 1)
(check-equal? (length (third schema)) 1)

Expand Down Expand Up @@ -180,7 +183,21 @@ RKT
"macos-host" "Sources" "RivetHost" "GeneratedBackend.swift")))
(define cpp
(file->string
(build-path project-root "windows" "GeneratedBackend.hpp")))
(build-path project-root "windows" "GeneratedBackend.hpp"))) ;; Bounded-wait companions (issue #252): every RPC gains a _within
;; variant that resolves through a promise, waits up to the timeout,
;; and cancels the pending request on a miss. Void RPCs return bool;
;; other results return std::nullopt on timeout.
(check-true
(string-contains?
cpp "std::optional<std::int64_t> increment_within(std::int64_t value, std::chrono::milliseconds rivet_timeout)"))
(check-true (string-contains? cpp "backend_.cancel(rivet_id); return std::nullopt;"))
(check-true (string-contains? cpp "bool log_line_within(std::string message, std::chrono::milliseconds rivet_timeout)"))
(check-true (string-contains? cpp "backend_.cancel(rivet_id); return false;"))
(check-true (string-contains? cpp "rivet_done->set_value(detail::decode_Int64(*rivet_result.value));"))
;; A Void async handler must not assign into Result<void>, which has no
;; value member — the decode only validates arrival.
(check-false (string-contains? cpp "result.value = detail::decode_void"))

(define linux-cpp
(file->string
(build-path project-root "linux" "GeneratedBackend.hpp")))
Expand Down Expand Up @@ -287,7 +304,7 @@ RKT
(check-equal? (length (hash-ref baseline 'records)) 1)
(check-equal?
(map (lambda (entry) (hash-ref entry 'name)) (hash-ref baseline 'rpcs))
'("echo-user" "greet" "increment"))
'("echo-user" "greet" "increment" "log-line"))
;; Enum support extends snapshot format v1 additively. A baseline written
;; by the pre-Enum v1 tool omitted the key and means an empty enum set.
(define pre-enum-baseline-path
Expand Down Expand Up @@ -315,10 +332,21 @@ RKT
(define-rpc (greet [name String] : String) name)
(define-rpc (increment [value Int64] : Int64) (add1 value))
(define-rpc (echo-user [user : User] : User) user)
(define-rpc (log-line [message String] : Void) (displayln message))
(define-rpc (health : Bool) #t)
(define (start in-fd out-fd) (serve-fds in-fd out-fd))
RKT
)
;; Zero-argument RPCs must not emit a stray comma before the timeout
;; (the taskboard's timing-start family broke CI before this pin).
(void (generate-clients! project))
(define zero-arg-cpp
(file->string (build-path project-root "windows" "GeneratedBackend.hpp")))
(check-true
(string-contains?
zero-arg-cpp
"std::optional<bool> health_within(std::chrono::milliseconds rivet_timeout)"))

(define compatible-report
(check-schema-compatibility! project baseline-path))
(check-true (hash-ref compatible-report 'compatible))
Expand All @@ -344,6 +372,7 @@ RKT
(define-rpc (greet [name String] : Bytes) #"")
(define-rpc (increment [value Int64] : Int64) (add1 value))
(define-rpc (echo-user [user : User] : User) user)
(define-rpc (log-line [message String] : Void) (displayln message))
(define (start in-fd out-fd) (serve-fds in-fd out-fd))
RKT
)
Expand Down Expand Up @@ -372,6 +401,7 @@ RKT
(define-rpc (greet [name String] : String) name)
(define-rpc (increment [value Int64] : Int64) (add1 value))
(define-rpc (echo-user [user : User] : User) user)
(define-rpc (log-line [message String] : Void) (displayln message))
(define (start in-fd out-fd) (serve-fds in-fd out-fd))
RKT
)
Expand Down
Loading