docs
Loading...
Searching...
No Matches
API.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5#pragma once
6// X11's headers define None as a macro, which would break the None enumerators.
7#pragma push_macro("None")
8#undef None
9
10#include <Ultralight/CAPI/CAPI_JSAPI.h>
11#include <Ultralight/CAPI/CAPI_String.h>
14#include <Ultralight/RefPtr.h>
15#include <Ultralight/View.h>
16#include <Ultralight/detail/InjectionFilter.h>
18#include <Ultralight/js/Error.h>
21#include <Ultralight/js/Value.h>
22#include <Ultralight/js/detail/Binding.h>
23#include <Ultralight/js/detail/Json.h>
24#include <Ultralight/js/detail/Schema.h>
25
26#include <cstdint>
27#include <initializer_list>
28#include <optional>
29#include <string>
30#include <string_view>
31#include <tuple>
32#include <type_traits>
33#include <utility>
34
35namespace ultralight {
36namespace js {
37
38template <typename T>
39class ClassBuilder;
40
41///
42/// The flags for API::AttachTo() (the C++ form of ULJSAPIAttachFlags).
43///
44enum class AttachFlags : unsigned {
45 None = 0, ///< Main frame only, with frozen namespace objects.
46 AllFrames = 1u << 1, ///< Also add the bindings to subframes.
47 Mutable = 1u << 2, ///< Don't freeze the namespace objects (page script can change or
48 ///< replace the bindings).
49};
50
51///
52/// Combine attach flags.
53///
54/// @return Returns the union of both flag sets.
55///
57 return static_cast<AttachFlags>(static_cast<unsigned>(a) | static_cast<unsigned>(b));
58}
59
60///
61/// Add attach flags to `a`.
62///
63/// @return Returns `a`.
64///
66 return a = a | b;
67}
68
69///
70/// Shorthand for AttachFlags::AllFrames.
71///
73
74///
75/// Shorthand for AttachFlags::Mutable.
76///
78
79static_assert(static_cast<unsigned>(AttachFlags::AllFrames) == kULJSAPIAttachFlags_AllFrames
80 && static_cast<unsigned>(AttachFlags::Mutable) == kULJSAPIAttachFlags_Mutable,
81 "js::AttachFlags must mirror ULJSAPIAttachFlags");
82
83///
84/// Options for API::AttachTo().
85///
86/// ```
87/// api.AttachTo(view.get(), { .flags = js::AllFrames,
88/// .origin_rules = { "https://*.mygame.com" } });
89/// ```
90///
92 ///
93 /// The attach flags (see AttachFlags).
94 ///
96
97 ///
98 /// The origin rules for the pages that get the bindings. Leave it empty for the default
99 /// policy (your application's own content). See ultralight::OriginRules.
100 ///
102};
103
104///
105/// The diagnostics levels for an API.
106///
107/// Diagnostics are development warnings, written to the native logger and to the page's
108/// console. Each level does everything the level below it does.
109///
110/// **Warn** reports mistakes that still run:
111///
112/// - **Typos**: reading a missing property of the API's namespace objects logs the closest
113/// existing name (`myApp.addd is not defined; did you mean add?`).
114/// - **Unknown events**: subscribing to or emitting an event the API never declared (checked
115/// once the API declares at least one event, see DefineEvent()).
116/// - **Lossy numbers**: a fractional or out-of-range argument truncated to an integer
117/// parameter, or a 64-bit integer return value that a JavaScript number can't hold exactly.
118/// - **Withheld bindings**: a page that the origin rules or the View's filter kept the
119/// bindings from.
120/// - **Reserved member names**: a member named `then`, `toJSON`, `constructor`, or
121/// `__proto__`, which changes how JavaScript treats the object.
122///
123/// **Strict** also turns two caller mistakes into TypeErrors:
124///
125/// - **A lossy numeric argument** (error code `ULJS_LOSSY`).
126/// - **More arguments than the binding declares** (error code `ULJS_EXTRA_ARGS`). A callable
127/// that takes a CallInfo is exempt, since it reads the raw argument list.
128///
129/// A lossy return value still only warns, since the page can't fix a native return type.
130///
131/// **Auto** (the default) uses the JavaScript level in Config::diagnostics, which is Warn in
132/// developer mode and Off otherwise (see DiagnosticsConfig).
133///
134/// Two warnings follow the Config's JavaScript level instead: an operation on a handle whose
135/// page is gone, and a JavaScript exception that nothing read. Mistakes found once at
136/// registration (an invalid or reserved root path, an invalid binding path, a missing callback)
137/// always warn, whatever the level.
138///
139/// \parblock
140/// @note Warn and Strict make property reads on the API's namespace objects slower, and
141/// attaching the API to a View also attaches the library's `ul.diagnostics` API.
142/// \endparblock
143///
144/// \parblock
145/// @note The UL_JS_DIAGNOSTICS environment variable ("off", "warn", or "strict", any case,
146/// read once per process) overrides the level of every API while developer mode is on,
147/// so you can switch diagnostics without recompiling.
148/// \endparblock
149///
151
152///
153/// Options for creating an API.
154///
155/// ```
156/// js::API api("myApp", { .diagnostics = js::Diagnostics::Strict });
157/// ```
158///
160 ///
161 /// The diagnostics level.
162 ///
163 /// Diagnostics::Auto uses the JavaScript level in Config::diagnostics, so you can switch
164 /// every API from one place.
165 ///
166 Diagnostics diagnostics = Diagnostics::Auto;
167};
168
169///
170/// A parameter name for a registration (shown in API::schema() and in error messages).
171///
172/// API::Bind(), API::DefineEvent(), ClassBuilder::Method(), and ClassBuilder::StaticMethod()
173/// accept these after the callable. Name every parameter (except a CallInfo or js::Resolver) or
174/// none. The text is read during the call, so it doesn't need to outlive it.
175///
176/// ```
177/// api.Bind("setConfig", [](double volume, bool muted) { ... },
178/// js::Param("volume"), js::Param("muted"));
179/// ```
180///
181/// A wrong call from the page then reports the names:
182///
183/// ```
184/// myApp.setConfig('loud', true);
185/// // TypeError: myApp.setConfig(volume: number, muted: boolean): argument 1 (volume):
186/// // expected number, got 'loud'
187/// ```
188///
189/// \parblock
190/// @note The assignment form (`api["x"] = fn`) takes no annotations. Use Bind() to add them.
191/// \endparblock
192///
193/// \parblock
194/// @note Don't confuse this with js::Arg, which is an argument value a callback reads.
195/// \endparblock
196///
197/// @see js::Doc
198///
199struct Param {
200 ///
201 /// The parameter name.
202 ///
203 const char* name;
204
205 ///
206 /// Create a parameter name annotation.
207 ///
208 /// @param declared_name The parameter name (NULL becomes an empty name).
209 ///
210 constexpr explicit Param(const char* declared_name)
211 : name(declared_name ? declared_name : "") {}
212};
213
214///
215/// A documentation string for a registration (shown in API::schema()).
216///
217/// The text becomes the entry's `doc` field in API::schema(). Pass at most one, to the same calls
218/// that take js::Param. The text is read during the call, so it doesn't need to outlive it.
219///
220/// ```
221/// api.Bind("save", &SaveGame, js::Doc("Saves the current session to disk."));
222/// ```
223///
224struct Doc {
225 ///
226 /// The documentation text.
227 ///
228 const char* text;
229
230 ///
231 /// Create a documentation annotation.
232 ///
233 /// @param doc_text The documentation text.
234 ///
235 constexpr explicit Doc(const char* doc_text) : text(doc_text) {}
236};
237
238namespace detail {
239
240// The trailing annotation pack accepted by the explicit registration forms. A concept
241// (rather than accepting anything) keeps receiver overloads unambiguous.
242template <typename A>
243concept Annotation = std::is_same_v<std::remove_cvref_t<A>, Param>
244 || std::is_same_v<std::remove_cvref_t<A>, Doc>;
245
246template <typename... Anns>
247inline constexpr size_t kArgAnnotationCount
248 = ((std::is_same_v<std::remove_cvref_t<Anns>, Param> ? size_t { 1 } : size_t { 0 }) + ...
249 + size_t { 0 });
250
251template <typename... Anns>
252inline constexpr size_t kDocAnnotationCount
253 = ((std::is_same_v<std::remove_cvref_t<Anns>, Doc> ? size_t { 1 } : size_t { 0 }) + ...
254 + size_t { 0 });
255
256inline void CollectAnnotation(AnnotationSet& set, const Param& annotation) {
257 if (set.name_count < AnnotationSet::kMaxNames)
258 set.names[set.name_count++] = annotation.name;
259}
260
261inline void CollectAnnotation(AnnotationSet& set, const Doc& annotation) {
262 set.doc = annotation.text;
263}
264
265template <typename... Anns>
266AnnotationSet CollectAnnotations(const Anns&... annotations) {
267 AnnotationSet set;
268 (CollectAnnotation(set, annotations), ...);
269 return set;
270}
271
272} // namespace detail
273
274///
275/// A global JavaScript namespace for native functions and data.
276///
277/// A js::API gives page scripts access to your application's functions and data, much like browsers
278/// add built-in Web APIs such as `console` and `document` on top of core JavaScript.
279///
280/// Once attached to a View, the API provides its global namespace to your pages.
281///
282/// Define an API with a constant and a function, attach it to a View, and load a page:
283///
284/// ```
285/// js::API app("app");
286/// app["version"] = "2.1.0";
287/// app["add"] = [](double a, double b) { return a + b; };
288///
289/// if (app.AttachTo(view.get()))
290/// view->LoadURL("file:///app.html");
291/// ```
292///
293/// Page script accesses the namespace on `window`:
294///
295/// ```js
296/// console.log(app.version); // "2.1.0"
297/// app.add(2, 3); // 5
298/// ```
299///
300/// ## Binding Functions and Values
301///
302/// The C++ type you bind decides what the page gets:
303///
304/// | Binding | What the Page Gets |
305/// |-------------------|--------------------------------|
306/// | Callable | Function |
307/// | Plain value | Read-only constant |
308/// | Getter and setter | Live property |
309/// | Async callable | Function returning a Promise |
310/// | Bound class | Class constructible with `new` |
311///
312/// A dot in a path string (such as `"fs.readFile"`) creates a child namespace object on the page.
313///
314/// Bind nested paths using dot notation and live properties using getters and setters:
315///
316/// ```
317/// app["fs.readFile"] = [](std::string path) { return ReadFile(path); };
318/// app.BindProperty("volume", [] { return g_volume; },
319/// [](double v) { g_volume = v; });
320/// ```
321///
322/// ## Attaching to a View
323///
324/// Call AttachTo() with a View before loading content. Every page the View loads from then on gets
325/// the API automatically, so you don't need to attach again after a navigation.
326///
327/// If you add, replace, or remove bindings after a page is loaded, that page keeps its current
328/// bindings until its next navigation. To adjust frame scope or mutability, pass AttachOptions to
329/// AttachTo() (see AttachFlags).
330///
331/// ## Allowed Pages and Origin Rules
332///
333/// By default, only local `file://` pages and content loaded with View::LoadHTML() get the API.
334///
335/// Passing origin rules replaces the default policy completely. Only pages matching your rules get
336/// the API, so you must include `"file://*"` if you want local pages to retain access (see
337/// ultralight::OriginRules). For decisions origin rules can't express (such as checking a user
338/// setting at runtime), set a callback with js::SetInjectionFilter() to allow or withhold the API
339/// per page.
340///
341/// Specify origin rules in AttachOptions to allow a local development server alongside local pages:
342///
343/// ```
344/// // A local development server, plus your local pages:
345/// if (app.AttachTo(view.get(),
346/// { .origin_rules = { "http://localhost:*", "file://*" } }))
347/// view->LoadURL("http://localhost:5173/");
348/// ```
349///
350/// ## Emitting Events
351///
352/// Native code can push events to attached pages without polling. Events are delivered on the
353/// Renderer's thread during a later Renderer::Update().
354///
355/// Call Emit() from any thread with an event name and payload arguments:
356///
357/// ```
358/// app.Emit("saved", std::string("slot1.dat"));
359/// ```
360///
361/// Subscribe on the page using on() or once(), and remove listeners with off():
362///
363/// ```js
364/// app.on("saved", (path) => refreshList(path));
365/// ```
366///
367/// ## Managing API Lifetime
368///
369/// A View keeps an API attached only while the js::API instance exists. Destroying the js::API
370/// detaches it from every View. Events stop reaching pages, and any calls to bound functions or
371/// class constructors that a page still holds throw a TypeError with code `ULJS_DETACHED`.
372///
373/// Hold the API as a class member to tie its lifetime to an owner and bind member functions with
374/// js::Bind():
375///
376/// ```
377/// class Game {
378/// public:
379/// explicit Game(View* view) {
380/// api_["showMenu"] = js::Bind(this, &Game::ShowMenu);
381/// if (api_.AttachTo(view))
382/// view->LoadURL("file:///app.html");
383/// }
384///
385/// void ShowMenu();
386///
387/// private:
388/// js::API api_{"app"}; // detaches from every View when the Game goes away
389/// };
390/// ```
391///
392/// @note Once an API is attached to a View, you must bind and unbind only on the Renderer's
393/// thread.
394///
395/// @see ultralight::OriginRules, js::SetInjectionFilter(), js::BindingGuard, js::ClassBuilder,
396/// js::TypeTraits, js::Diagnostics
397///
398class API {
399 public:
400 ///
401 /// Create an API rooted at a global namespace path.
402 ///
403 /// @param root_path The dot-separated namespace path (eg, "myApp" or "myApp.native"). Every
404 /// segment must be non-empty. The roots `ul` and `ultralight` (and every
405 /// path under them) are reserved for the library.
406 ///
407 /// @param options The creation options (see APIOptions).
408 ///
409 /// @note An invalid or reserved `root_path` creates an empty API (`operator bool` returns
410 /// false) and logs a warning saying why.
411 ///
412 explicit API(const char* root_path, const APIOptions& options = {})
413 : root_(root_path ? root_path : ""), api_(ulCreateJSAPI(root_path)) {
414 if (api_ && options.diagnostics != Diagnostics::Auto)
415 ulJSAPISetDiagnostics(api_, static_cast<ULJSDiagnosticsLevel>(options.diagnostics));
416 }
417
418 ///
419 /// Destructor (releases this handle).
420 ///
421 /// @note Destroying the API detaches it from every View (see Managing API Lifetime in the class
422 /// overview), unless it came from FromBorrowed() or another owning handle to it exists.
423 ///
424 ~API() { ulDestroyJSAPI(api_); }
425
426 ///
427 /// Move constructor (`other` becomes empty).
428 ///
429 API(API&& other) noexcept : root_(std::move(other.root_)), api_(other.api_) {
430 other.api_ = nullptr;
431 }
432
433 ///
434 /// Move assignment (releases the current handle, and `other` becomes empty).
435 ///
436 API& operator=(API&& other) noexcept {
437 if (this != &other) {
438 ulDestroyJSAPI(api_);
439 root_ = std::move(other.root_);
440 api_ = other.api_;
441 other.api_ = nullptr;
442 }
443 return *this;
444 }
445 API(const API&) = delete;
446 API& operator=(const API&) = delete;
447
448 // --- Interop with the C API (most embedders never touch raw handles) -------------------
449
450 ///
451 /// Wrap a handle you own from the C API, taking ownership of it (eg, the result of
452 /// ulCreateJSAPI()).
453 ///
454 /// @param api The handle to take ownership of.
455 ///
456 /// @return Returns an API that owns `api`.
457 ///
458 /// @note For a handle someone else owns, use FromBorrowed().
459 ///
460 static API Adopt(ULJSAPI api) { return API(api); }
461
462 ///
463 /// Wrap a handle someone else owns, without owning the API (eg, the `api` member of a
464 /// js::InjectionRequest).
465 ///
466 /// The result works like any API, but destroying it never detaches the API, and it doesn't
467 /// keep the API attached once its owners are gone.
468 ///
469 /// @param api The borrowed handle.
470 ///
471 /// @return Returns an API with its own (non-owning) reference to `api`. Its raw() is a
472 /// different handle than `api`.
473 ///
474 /// @note For a handle you own, use Adopt().
475 ///
476 static API FromBorrowed(ULJSAPI api) { return API(ulCreateJSAPIBorrowedRef(api)); }
477
478 ///
479 /// Whether or not this API holds a handle (false if the root path was invalid or reserved, and
480 /// after a move or LeakRef()).
481 ///
482 explicit operator bool() const { return api_ != nullptr; }
483
484 ///
485 /// The assignable path that operator[] returns (see Binding Functions and Values in the class
486 /// overview).
487 ///
488 /// @warning Use a Binder only in the expression that created it (`api["path"] = ...`). It
489 /// can't be copied, and a named Binder can't bind, since the API could move or be
490 /// destroyed while you hold it. Keep the path string instead.
491 ///
492 class Binder {
493 public:
494 Binder(const Binder&) = delete;
495 Binder& operator=(const Binder&) = delete;
496
497 ///
498 /// Bind a callable or value at this path (the type of `rhs` decides what's bound, see Binding
499 /// Functions and Values in the API class overview).
500 ///
501 /// @param rhs The callable or value to bind.
502 ///
503 /// @return Returns this Binder.
504 ///
505 template <typename T>
506 Binder& operator=(T&& rhs) && {
507 using U = std::decay_t<T>;
508 if constexpr (detail::IsIntrospectableCallable<U>::value) {
509 api_->Bind(path_.c_str(), std::forward<T>(rhs));
510 } else if constexpr (Holder<U>) {
511 // A holder of a bound class exposes the INSTANCE: a read-only live property whose
512 // every read returns the instance's wrapper (one wrapper per page, so `===` holds).
514 "an exclusive holder cannot back a live instance binding (every page "
515 "read needs its own stake): bind a shared holder such as RefPtr<T>, "
516 "std::shared_ptr<T>, or js::Eternal<T>");
517 static_assert(Marshalable<U>,
518 "bind the class first: include <Ultralight/js/Class.h> and call "
519 "DefineClass<T>() before exposing an instance of it");
520 api_->BindProperty(path_.c_str(), [value = U(std::forward<T>(rhs))] { return value; });
521 } else if constexpr (LockableHolder<U>) {
522 static_assert(detail::kAlwaysFalse<U>,
523 "a weak holder cannot back a live instance binding (every page read "
524 "needs a strong ownership stake): bind a shared holder such as "
525 "RefPtr<T>, or bind a method with js::Bind(weak, &T::Method)");
526 } else if constexpr (std::is_null_pointer_v<U>) {
527 static_assert(detail::kAlwaysFalse<U>,
528 "nullptr is not a JavaScript value: use js::null for a JavaScript null");
529 } else if constexpr (detail::kIsJsonConstant<U>
530 || std::is_convertible_v<U, std::string_view>) {
531 api_->SetConstant(path_.c_str(), std::forward<T>(rhs));
532 } else if constexpr (Marshalable<U>) {
533 static_assert(detail::kAlwaysFalse<U>,
534 "this type cannot be a constant: constants are serialized to JSON "
535 "once at registration (no live context exists yet), and JSON cannot "
536 "represent it. Bind a function or property instead (js::undefined "
537 "has no JSON form; use js::null)");
538 } else {
539 static_assert(detail::kAlwaysFalse<U>,
540 "this value can be neither bound nor stored as a constant. Callables "
541 "must have exactly one non-template operator() (generic lambdas with "
542 "'auto' parameters cannot be introspected); constants must be one of "
543 "the JSON-serializable types (see API::SetConstant())");
544 }
545 return *this;
546 }
547
548 ///
549 /// Get the Binder for a child path (`api["fs"]["readFile"]` is `api["fs.readFile"]`).
550 ///
551 /// @param segment The child path segment.
552 ///
553 /// @return Returns the Binder for the combined path.
554 ///
555 Binder operator[](const char* segment) && {
556 return Binder(api_, path_ + "." + (segment ? segment : ""));
557 }
558
559 ///
560 /// Bind a property with a getter (and optional setter) at this path (see
561 /// API::BindProperty()):
562 ///
563 /// ```
564 /// api["player"]["health"].BindProperty([&] { return player.health; });
565 /// ```
566 ///
567 /// @param get The getter, called on every JavaScript read.
568 ///
569 /// @param set The setter, or nullptr for a read-only property.
570 ///
571 /// @note Assigning a callable always binds a function the page calls with parentheses,
572 /// even one that takes no arguments. Use this to bind a property instead.
573 ///
574 template <typename Getter, typename Setter = std::nullptr_t>
575 void BindProperty(Getter&& get, Setter&& set = nullptr) && {
576 api_->BindProperty(path_.c_str(), std::forward<Getter>(get),
577 std::forward<Setter>(set));
578 }
579
580 private:
581 friend class API;
582 Binder(API* api, std::string path) : api_(api), path_(std::move(path)) {}
583 API* api_;
584 std::string path_;
585 };
586
587 ///
588 /// Get the Binder for a path relative to the root (see Binder).
589 ///
590 /// @param path The dot-separated path (eg, "greet" or "fs.readFile").
591 ///
592 /// @return Returns the Binder for `path`.
593 ///
594 Binder operator[](const char* path) { return Binder(this, path ? path : ""); }
595
596 ///
597 /// Bind a callable at a path (the explicit form of `api[path] = fn`, which also takes
598 /// annotations).
599 ///
600 /// @param path The dot-separated path relative to the root (eg, "fs.readFile").
601 ///
602 /// @param fn The callable to bind (see Binding Functions and Values in the class
603 /// overview).
604 ///
605 /// @param annotations Optional js::Param names (one per parameter, or none) and at most one
606 /// js::Doc.
607 ///
608 /// @note Binding a path that's already bound replaces the old binding and removes the path's
609 /// metadata (so call SetMetadata() after binding).
610 ///
611 template <typename Fn, typename... Anns>
612 requires(detail::Annotation<Anns> && ...)
613 void Bind(const char* path, Fn&& fn, const Anns&... annotations) {
614 using F = std::decay_t<Fn>;
615 using Traits = detail::CallableTraits<F>;
616 using Shape = detail::ParamShape<typename Traits::Params>;
617 using R = typename Traits::Return;
618
619 [&]<size_t... Is>(std::index_sequence<Is...>) {
620 (detail::ValidateParam<std::tuple_element_t<Is, typename Shape::Converted>>(), ...);
621 }(std::make_index_sequence<Shape::kCount> {});
622 constexpr size_t kNames = detail::kArgAnnotationCount<Anns...>;
623 static_assert(kNames == 0 || kNames == Shape::kCount,
624 "js::Param annotations must name every payload parameter or none (one "
625 "Param per converted parameter; CallInfo and Resolver take no name)");
626 static_assert(detail::kDocAnnotationCount<Anns...> <= 1,
627 "at most one js::Doc annotation per registration");
628
629 detail::AnnotationSet set = detail::CollectAnnotations(annotations...);
630 auto* bound
631 = new detail::BoundCallable<F> { std::forward<Fn>(fn), FullPath(path), {}, {} };
632 bound->display
633 = bound->path + detail::RenderSignature<typename Shape::Converted>(set);
634 bound->names.assign(set.names, set.names + set.name_count);
635 if constexpr (detail::HasSettableBoundPath<F>)
636 bound->fn.SetBoundPath(bound->path);
637 bool bound_ok;
638 if constexpr (Shape::kHasResolver || detail::kIsAsyncReturn<R>) {
639 bound_ok = ulJSAPIBindAsyncFunction(api_, path, &detail::AsyncTrampoline<F>, bound,
640 &detail::DestroyBoundCallable<F>);
641 } else {
642 bound_ok = ulJSAPIBindFunction(api_, path, &detail::SyncTrampoline<F>, bound,
643 &detail::DestroyBoundCallable<F>);
644 }
645 if (!bound_ok)
646 return;
647
648 detail::SchemaShapes shapes;
649 std::string fragment
650 = detail::BuildCallableFragment<typename Shape::Converted, R, Shape::kHasResolver>(
651 shapes, set);
652 ulJSAPISetMetadata(api_, path, fragment.c_str());
653 detail::EmitSchemaShapes(api_, shapes);
654 }
655
656 ///
657 /// Bind a member function of an object at a path (the same as binding
658 /// `js::Bind(receiver, method)`):
659 ///
660 /// ```
661 /// api.Bind("save", this, &App::OnSave);
662 /// ```
663 ///
664 /// @param path The dot-separated path relative to the root (eg, "fs.readFile").
665 ///
666 /// @param receiver The object to call `method` on. It must outlive every call through the
667 /// binding.
668 ///
669 /// @param method The member function pointer.
670 ///
671 /// @param annotations Optional js::Param names (one per parameter, or none) and at most one
672 /// js::Doc.
673 ///
674 /// @note To have the binding keep the object alive, bind `js::Bind(holder, method)` with a
675 /// shared holder instead.
676 ///
677 template <typename T, typename M, typename... Anns>
678 requires(std::is_member_function_pointer_v<M> && (detail::Annotation<Anns> && ...))
679 void Bind(const char* path, T* receiver, M method, const Anns&... annotations) {
680 Bind(path, detail::MakeBoundMethod(receiver, method, detail::MemberFnTraits<M> {}),
681 annotations...);
682 }
683
684 ///
685 /// Set a constant at a path (the explicit form of `api[path] = value`).
686 ///
687 /// The value is copied when you call this, and every page gets that copy. Changing your C++
688 /// value later doesn't change the constant (use BindProperty() for a live value). Page script
689 /// can't modify a constant, and objects and arrays in it are frozen too.
690 ///
691 /// @param path The dot-separated path relative to the root.
692 ///
693 /// @param value The value (see the supported types below).
694 ///
695 /// \parblock
696 /// @note Constants support the JSON-serializable types: numbers, booleans, strings, js::null,
697 /// std::optional, std::vector, std::map<std::string, T>, and (when
698 /// ULTRALIGHT_REFLECTION is 1) enums and plain aggregate structs of these.
699 /// js::undefined has no JSON form, so use js::null instead.
700 /// \endparblock
701 ///
702 /// \parblock
703 /// @note Structs convert field by field, so a custom js::TypeTraits specialization doesn't
704 /// apply to a constant. Bind a function or property for a custom conversion.
705 /// \endparblock
706 ///
707 /// \parblock
708 /// @note A null `const char*` binds `null` (and logs a diagnostics warning). For `null` on
709 /// purpose, pass js::null.
710 /// \endparblock
711 ///
712 template <typename T>
713 void SetConstant(const char* path, T&& value) {
714 using U = std::decay_t<T>;
715 if constexpr (std::is_same_v<U, const char*> || std::is_same_v<U, char*>) {
716 if (!value) {
717 ulJSAPISetConstantJSON(api_, path, "null");
718 if (ulJSAPIGetDiagnostics(api_) >= kULJSDiagnostics_Warn) {
719 std::string message = FullPath(path)
720 + ": the constant's string is a null pointer, so it was bound as null.";
721 ulJSAPIEmitDiagnostic(nullptr, message.c_str());
722 }
723 return;
724 }
725 }
726 if constexpr (std::is_null_pointer_v<U>) {
727 static_assert(detail::kAlwaysFalse<U>,
728 "nullptr is not a JavaScript value: use js::null for a JavaScript null");
729 } else if constexpr (std::is_same_v<U, bool>) {
730 ulJSAPISetConstantBoolean(api_, path, value);
731 } else if constexpr (std::is_arithmetic_v<U>) {
732 bool set = ulJSAPISetConstantNumber(api_, path, static_cast<double>(value));
733 // The generated constant entry says "number"; an integral C++ source refines it.
734 if constexpr (std::is_integral_v<U>) {
735 if (set)
736 ulJSAPISetMetadata(api_, path, "{\"type\":\"integer\"}");
737 }
738 } else if constexpr (std::is_convertible_v<U, std::string_view>) {
739 std::string text { std::string_view(value) };
740 ulJSAPISetConstantString(api_, path, text.c_str());
741 } else {
742 std::string json;
743 detail::WriteJson(json, value);
744 bool set = ulJSAPISetConstantJSON(api_, path, json.c_str());
745 // A JSON constant's generated entry carries the parsed value but no type; the typed
746 // layer knows it (js::null stays untyped: "null" is not a schema expression).
747 if constexpr (!std::is_same_v<U, NullType>) {
748 if (!set)
749 return;
750 detail::SchemaShapes shapes;
751 std::string fragment = detail::BuildTypeFragment<U>(shapes);
752 ulJSAPISetMetadata(api_, path, fragment.c_str());
753 detail::EmitSchemaShapes(api_, shapes);
754 }
755 }
756 }
757
758 ///
759 /// Define a native class at a path, so page script can call its methods, read its properties,
760 /// and construct it with `new` (when you declare a constructor).
761 ///
762 /// ```
763 /// api.DefineClass<Database>("Database")
764 /// .Constructor<std::string>()
765 /// .Method("query", &Database::Query);
766 /// ```
767 ///
768 /// @param path The dot-separated path relative to the root. The first DefineClass() for a
769 /// type also takes the JavaScript class name from the last path segment
770 /// ("db.Database" gives a class called "Database").
771 ///
772 /// @return Returns the builder to declare members on. The class is registered when the
773 /// builder goes out of scope (normally at the end of the statement).
774 ///
775 /// @note Requires `<Ultralight/js/Class.h>`. A type binds to one class for the life of the
776 /// process, so a later DefineClass() for the same type reuses that class and its
777 /// members (see ClassBuilder).
778 ///
779 /// @see ClassBuilder
780 ///
781 template <typename T>
782 ClassBuilder<T> DefineClass(const char* path);
783
784 ///
785 /// Bind a property with a getter and an optional setter.
786 ///
787 /// Unlike a constant, the property is live: every JavaScript read calls `get`, and every
788 /// assignment calls `set`.
789 ///
790 /// ```
791 /// api.BindProperty("volume", [] { return g_volume; },
792 /// [](double v) { g_volume = v; });
793 /// // Page: myApp.volume -> calls the getter
794 /// // myApp.volume = 0.5 -> calls the setter
795 /// ```
796 ///
797 /// @param path The dot-separated path relative to the root.
798 ///
799 /// @param get The getter. It takes no parameters and returns a type js::TypeTraits
800 /// converts, or a js::Result<T> to throw an error into the page.
801 ///
802 /// @param set The setter, which takes one parameter of a type js::TypeTraits converts
803 /// (return a js::Result<void> to reject the assignment with an error). Pass
804 /// nullptr for a read-only property: assigning to it then throws in strict-mode
805 /// code and does nothing otherwise.
806 ///
807 /// @note Assigning a value of the wrong type throws the standard TypeError, and the setter
808 /// isn't called.
809 ///
810 template <typename Getter, typename Setter = std::nullptr_t>
811 void BindProperty(const char* path, Getter&& get, Setter&& set = nullptr) {
812 using G = std::decay_t<Getter>;
813 using GTraits = detail::CallableTraits<G>;
814 static_assert(std::tuple_size_v<typename GTraits::Params> == 0,
815 "a property getter takes no parameters");
816 static_assert(!std::is_void_v<typename GTraits::Return>,
817 "a property getter must return a value (a Marshalable type or "
818 "js::Result<T>)");
819 if constexpr (std::is_same_v<std::decay_t<Setter>, std::nullptr_t>) {
820 struct ReadOnly {
821 void operator()() const {}
822 };
823 using Bound = detail::BoundProperty<G, ReadOnly>;
824 auto* bound = new Bound { std::forward<Getter>(get), ReadOnly {}, FullPath(path) };
825 if constexpr (detail::HasSettableBoundPath<G>)
826 bound->get.SetBoundPath(bound->path);
827 if (!ulJSAPIBindProperty(api_, path, &detail::PropertyGetterThunk<Bound>, nullptr, bound,
828 &detail::DestroyBoundProperty<Bound>))
829 return;
830 } else {
831 using S = std::decay_t<Setter>;
832 using Bound = detail::BoundProperty<G, S>;
833 auto* bound = new Bound { std::forward<Getter>(get), std::forward<Setter>(set),
834 FullPath(path) };
835 if constexpr (detail::HasSettableBoundPath<G>)
836 bound->get.SetBoundPath(bound->path);
837 if constexpr (detail::HasSettableBoundPath<S>)
838 bound->set.SetBoundPath(bound->path);
839 if (!ulJSAPIBindProperty(api_, path, &detail::PropertyGetterThunk<Bound>,
840 &detail::PropertySetterThunk<Bound>, bound,
841 &detail::DestroyBoundProperty<Bound>))
842 return;
843 }
844
845 detail::SchemaShapes shapes;
846 std::string fragment = detail::BuildTypeFragment<typename GTraits::Return>(shapes);
847 ulJSAPISetMetadata(api_, path, fragment.c_str());
848 detail::EmitSchemaShapes(api_, shapes);
849 }
850
851 ///
852 /// Emit an event to the page listeners subscribed with `on` or `once` (see Emitting Events in
853 /// the class overview).
854 ///
855 /// ```
856 /// api.Emit("saved", std::string("slot1.dat"));
857 /// // Page: myApp.on('saved', (path) => { ... });
858 /// ```
859 ///
860 /// @param event_path The event path relative to the root (eg, "saved" or "fs.changed").
861 /// A page can subscribe to "fs.changed" on the root with the full path
862 /// (`myApp.on('fs.changed', fn)`), or as `myApp.fs.on('changed', fn)`
863 /// when something is bound under `fs` (only then does `myApp.fs` exist).
864 ///
865 /// @param args Up to 16 argument values of types js::TypeTraits converts.
866 ///
867 /// \parblock
868 /// @note Safe to call from any thread. The arguments are copied here (strings included), so
869 /// nothing you pass needs to outlive the call.
870 /// \endparblock
871 ///
872 /// \parblock
873 /// @note The event is delivered later, on the Renderer's thread: during a later
874 /// Renderer::Update(), the listeners run once in each page that had the bindings when
875 /// you called Emit() (each frame, for an API attached with AllFrames).
876 /// \endparblock
877 ///
878 /// @see dom::Element::dispatchEvent()
879 ///
880 template <typename... Args>
882 void Emit(const char* event_path, Args&&... args) {
883 static_assert(sizeof...(Args) <= 16, "events support at most 16 arguments");
884 static_assert((!detail::kIsClassInstancePointer<Args> && ...),
885 "a raw instance pointer cannot cross a deferred boundary (delivery "
886 "happens later, on the Renderer's thread): capture an owning holder (the "
887 "class's canonical holder) or js::Eternal<T> instead");
888 static_assert((!detail::IsCallScopedView<std::decay_t<Args>>::value && ...),
889 "a std::span cannot cross a deferred boundary (delivery happens later; "
890 "the view would dangle): copy into a std::vector<E> or RefPtr<Buffer>");
891 static_assert((!std::is_base_of_v<Value, std::decay_t<Args>> && ...),
892 "a js::Value cannot be emitted (Emit delivers to every attached page, and "
893 "a value belongs to one page): emit native values instead, such as "
894 "numbers, strings, containers, or structs. A js::ArrayBuffer or "
895 "js::TypedArray from the page can't be emitted either: emit a "
896 "RefPtr<Buffer> or the bytes (eg, a std::vector<uint8_t>)");
897 using Payload = std::tuple<detail::CapturedArg<Args>...>;
898 auto* payload = new Payload(detail::CaptureArg(std::forward<Args>(args))...);
899 ulJSAPIEmitEvent(api_, event_path, &detail::EmitArgsThunk<Payload>, payload,
900 &detail::DeleteEmitPayload<Payload>);
901 }
902
903 ///
904 /// Declare an event and its payload types.
905 ///
906 /// Declared events appear in schema(), and the generated TypeScript declarations give them
907 /// typed `on`, `once`, and `off` overloads. The declaration only describes the event, so Emit()
908 /// doesn't check its arguments against the declared types.
909 ///
910 /// ```
911 /// api.DefineEvent<std::string>("saved", js::Param("path"), js::Doc("A save finished."));
912 /// api.Emit("saved", path); // Page: myApp.on('saved', (path) => ...)
913 /// ```
914 ///
915 /// @param event_path The event path relative to the root, in the form Emit() takes.
916 ///
917 /// @param annotations Optional js::Param names (one per payload type, or none) and at most
918 /// one js::Doc.
919 ///
920 /// \parblock
921 /// @note Once an API declares an event, diagnostics (Warn and Strict) warn about every
922 /// subscription and every Emit() for an event it didn't declare. You should declare
923 /// every event the API emits.
924 /// \endparblock
925 ///
926 /// \parblock
927 /// @note Declaring an event again replaces its payload types. Declarations are permanent
928 /// (Unbind() never removes them).
929 /// \endparblock
930 ///
931 /// \parblock
932 /// @note This follows the registration thread rules (see the class overview).
933 /// \endparblock
934 ///
935 template <typename... Args, typename... Anns>
936 requires(detail::Annotation<Anns> && ...)
937 void DefineEvent(const char* event_path, const Anns&... annotations) {
938 static_assert(sizeof...(Args) <= 16, "events support at most 16 arguments");
939 static_assert((Marshalable<std::remove_cvref_t<Args>> && ...),
940 "an event payload type has no js::TypeTraits: use a supported built-in, "
941 "make it a plain aggregate struct, or specialize js::TypeTraits for it");
942 constexpr size_t kNames = detail::kArgAnnotationCount<Anns...>;
943 static_assert(kNames == 0 || kNames == sizeof...(Args),
944 "js::Param annotations must name every event parameter or none");
945 static_assert(detail::kDocAnnotationCount<Anns...> <= 1,
946 "at most one js::Doc annotation per registration");
947 detail::AnnotationSet set = detail::CollectAnnotations(annotations...);
948 detail::SchemaShapes shapes;
949 std::string fragment = detail::BuildEventFragment<Args...>(shapes, set, event_path);
950 ulJSAPISetMetadata(api_, "", fragment.c_str());
951 detail::EmitSchemaShapes(api_, shapes);
952 }
953
954 ///
955 /// Remove the binding at a path, along with its metadata.
956 ///
957 /// Pages see the removal at their next navigation. A page that's already loaded (or restored
958 /// from the back-forward cache) keeps the binding, and functions the page got from it keep
959 /// working. A js::Task one of them already started finishes normally. The library destroys
960 /// the bound callable once the last of those functions and Tasks is gone (right away if
961 /// there's none).
962 ///
963 /// To tie a binding to a C++ object's lifetime instead, see js::BindingGuard.
964 ///
965 /// @param path The binding's path relative to the root, as you bound it.
966 ///
967 /// @return Returns whether a binding or metadata entry was removed (false for an empty path
968 /// or a path with neither). You can ignore it, since removing an absent path is
969 /// harmless.
970 ///
971 /// \parblock
972 /// @note Event declarations and root metadata (the top-level `meta`, `events`, and `types`
973 /// sections) are never removed.
974 /// \endparblock
975 ///
976 /// \parblock
977 /// @note This follows the registration thread rules (see the class overview).
978 /// \endparblock
979 ///
980 bool Unbind(const char* path) { return ulJSAPIUnbind(api_, path); }
981
982 ///
983 /// Get the API's schema as JSON.
984 ///
985 /// The JSON has a `format` ("ul-schema"), an `api` ("js"), and a `version`, then an entry for
986 /// each binding with its parameter and return types, the named types they use, and the
987 /// metadata you added (js::Param, js::Doc, and SetMetadata()).
988 ///
989 /// A reader should ignore keys and types it doesn't know. The `version` changes only for a
990 /// change that would break an existing reader.
991 ///
992 /// @return Returns the JSON text (an empty string for an empty API).
993 ///
994 /// @note Safe to call from any thread.
995 ///
996 std::string schema() const {
997 ULString text = ulJSAPIGetSchema(api_);
998 std::string result
999 = text ? std::string(ulStringGetData(text), ulStringGetLength(text)) : std::string();
1000 ulDestroyString(text);
1001 return result;
1002 }
1003
1004 ///
1005 /// Write the JSON from schema() to a file.
1006 ///
1007 /// The library writes the file before this returns. A tool such as `gen-typescript.py` can
1008 /// then read it.
1009 ///
1010 /// @param utf8_path The file's path as a UTF-8 string (on Windows too). An existing file is
1011 /// replaced.
1012 ///
1013 /// @return Returns whether or not the file was written (false for an empty API, or a NULL or
1014 /// empty `utf8_path`). A write that fails also logs a warning with the path.
1015 ///
1016 /// @note Safe to call from any thread.
1017 ///
1018 bool DumpSchema(const char* utf8_path) { return ulJSAPIDumpSchema(api_, utf8_path); }
1019
1020 ///
1021 /// Add schema metadata (eg, documentation) to the entry at a path.
1022 ///
1023 /// schema() merges each fragment over the entry: objects merge field by field, other values
1024 /// replace, and later fragments win.
1025 ///
1026 /// Binding a path again (replacing its binding) removes that path's metadata, so call this
1027 /// after the Bind() it describes.
1028 ///
1029 /// ```
1030 /// api.SetMetadata("add", "{\"doc\": \"Adds two numbers.\"}");
1031 /// ```
1032 ///
1033 /// @param path The dot-separated path relative to the root, or "" for the document
1034 /// root (its top-level `meta`, `events`, and `types` sections). Root
1035 /// `events` entries declare events the way DefineEvent() does.
1036 ///
1037 /// @param metadata_json A JSON object. Anything else is ignored with a logged warning.
1038 ///
1039 /// @note This follows the registration thread rules (see the class overview).
1040 ///
1041 void SetMetadata(const char* path, const char* metadata_json) {
1042 ulJSAPISetMetadata(api_, path, metadata_json);
1043 }
1044
1045 ///
1046 /// Set the diagnostics level (see Diagnostics).
1047 ///
1048 /// Most warnings follow the new level right away. Typo warnings (reads of missing properties)
1049 /// start only on pages that get the bindings after you turn them on, but stop right away when
1050 /// you set Diagnostics::Off.
1051 ///
1052 /// @param level The new level (Diagnostics::Auto goes back to the Config's JavaScript
1053 /// level).
1054 ///
1055 /// \parblock
1056 /// @note You should set the level before attaching the API to a View (or pass it in
1057 /// APIOptions), since attaching at Warn or higher is what also attaches the library's
1058 /// `ul.diagnostics` API. Raising the level afterward doesn't add it.
1059 /// \endparblock
1060 ///
1061 /// \parblock
1062 /// @note The UL_JS_DIAGNOSTICS environment variable overrides this level while developer
1063 /// mode is on (see diagnostics()).
1064 /// \endparblock
1065 ///
1066 /// \parblock
1067 /// @note This follows the registration thread rules (see the class overview).
1068 /// \endparblock
1069 ///
1071 ulJSAPISetDiagnostics(api_, static_cast<ULJSDiagnosticsLevel>(level));
1072 }
1073
1074 ///
1075 /// Get the effective diagnostics level.
1076 ///
1077 /// @return Returns the level in effect: the UL_JS_DIAGNOSTICS environment override when it
1078 /// applies, else the level set on this API, with Diagnostics::Auto replaced by the
1079 /// Config's JavaScript level (this is never Diagnostics::Auto).
1080 ///
1082 return static_cast<Diagnostics>(ulJSAPIGetDiagnostics(api_));
1083 }
1084
1085 ///
1086 /// Attach this API to a View.
1087 ///
1088 /// The bindings are added to the View's current page (if the origin policy allows it) and to
1089 /// every page the View loads afterwards. The View keeps the API attached until you detach it
1090 /// or destroy this API. Attaching again updates the flags and origin rules.
1091 ///
1092 /// ```
1093 /// api.AttachTo(view.get()); // your own content
1094 /// api.AttachTo(view.get(), { .flags = js::AllFrames }); // and subframes
1095 /// api.AttachTo(view.get(), { .origin_rules = { "https://*.mygame.com" } }); // the game's site
1096 /// ```
1097 ///
1098 /// @param view The View to attach to.
1099 ///
1100 /// @param options The attach flags and the origin rules for the pages that get the bindings
1101 /// (see AttachOptions).
1102 ///
1103 /// @return Returns true on success, or false if `view` is nullptr, this API is empty, a flag
1104 /// is unknown, a rule failed to parse, or every owning handle to the API is gone (for
1105 /// an API from FromBorrowed()). Nothing changes then. An unknown flag, a rule that
1106 /// failed to parse, or a destroyed API also logs a warning that says why.
1107 ///
1108 /// \parblock
1109 /// @note Call this on the Renderer's thread.
1110 /// \endparblock
1111 ///
1112 /// \parblock
1113 /// @note APIs that share a namespace prefix (eg, roots `myApp.fs` and `myApp.net`) should all
1114 /// be attached before the page loads. An API attached later can't add to a frozen
1115 /// namespace the page already has, so its bindings appear after the page's next
1116 /// navigation.
1117 /// \endparblock
1118 ///
1119 [[nodiscard]] bool AttachTo(View* view, const AttachOptions& options = {}) {
1120 return view && api_
1121 && view->AttachJSAPI(api_, static_cast<unsigned>(options.flags),
1122 options.origin_rules.data(), options.origin_rules.size());
1123 }
1124
1125 ///
1126 /// Detach this API from a View.
1127 ///
1128 /// Events stop reaching the View's pages right away. The current page keeps the bindings it
1129 /// already has until it navigates (destroying the API instead makes them throw).
1130 ///
1131 /// @param view The View to detach from.
1132 ///
1133 /// @note Call this on the Renderer's thread.
1134 ///
1135 void DetachFrom(View* view) {
1136 if (view && api_)
1137 view->DetachJSAPI(api_);
1138 }
1139
1140 ///
1141 /// Get the underlying handle, for calls into the C API.
1142 ///
1143 /// @return Returns the handle (this API keeps ownership).
1144 ///
1145 ULJSAPI raw() const { return api_; }
1146
1147 ///
1148 /// Give up ownership of the underlying handle without releasing it (this API becomes empty).
1149 ///
1150 /// @return Returns the handle. You must call ulDestroyJSAPI() when finished.
1151 ///
1153 ULJSAPI api = api_;
1154 api_ = nullptr;
1155 return api;
1156 }
1157
1158 private:
1159 // The Adopt / FromBorrowed route: wrap an existing handle and recover its root path so
1160 // path-qualified diagnostics keep working.
1161 explicit API(ULJSAPI adopted) : api_(adopted) {
1162 if (!api_)
1163 return;
1164 ULString root = ulJSAPIGetRootPath(api_);
1165 if (root) {
1166 root_.assign(ulStringGetData(root), ulStringGetLength(root));
1167 ulDestroyString(root);
1168 }
1169 }
1170
1171 std::string FullPath(const char* path) const {
1172 std::string full = root_;
1173 full += '.';
1174 full += path ? path : "";
1175 return full;
1176 }
1177
1178 std::string root_;
1179 ULJSAPI api_;
1180};
1181
1182///
1183/// A binding tied to a C++ object's lifetime.
1184///
1185/// When an armed guard is destroyed, it removes the binding at its path (see API::Unbind()), so
1186/// a feature can keep a binding for exactly as long as its owner lives:
1187///
1188/// ```
1189/// api.Bind("debug.dump", [] { DumpState(); });
1190/// js::BindingGuard dump_guard(api, "debug.dump");
1191/// // ~dump_guard removes debug.dump (pages see the removal at their next navigation).
1192/// ```
1193///
1194/// The guard keeps its own reference to the API, so it stays safe to destroy after the js::API
1195/// that created it. It doesn't own the API, so destroying the js::API still detaches it. The
1196/// guard removes by path: if you bind something else at the path later, the guard removes that.
1197///
1198/// \parblock
1199/// @note Destroying or move-assigning over an armed guard removes the binding, so it follows
1200/// the registration thread rules (see the js::API class overview): once the API is
1201/// attached, do it on the Renderer's thread. To give up a guard on another thread, call
1202/// Release().
1203/// \endparblock
1204///
1205/// \parblock
1206/// @note `operator bool` only says whether the guard is armed. It never reflects whether the
1207/// binding or any page still exists.
1208/// \endparblock
1209///
1210class [[nodiscard]] BindingGuard {
1211 public:
1212 ///
1213 /// Create an unarmed guard (removes nothing).
1214 ///
1215 BindingGuard() = default;
1216
1217 ///
1218 /// Create an armed guard that removes the binding at `path` when destroyed.
1219 ///
1220 /// @param api The API the binding belongs to (an empty API gives an unarmed guard).
1221 ///
1222 /// @param path The binding's path relative to the root, as you bound it.
1223 ///
1224 BindingGuard(const API& api, const char* path)
1225 : api_(ulCreateJSAPIBorrowedRef(api.raw())), path_(path ? path : "") {}
1226
1227 ///
1228 /// Destructor (removes the binding if the guard is armed).
1229 ///
1231 if (api_) {
1232 ulJSAPIUnbind(api_, path_.c_str());
1233 ulDestroyJSAPI(api_);
1234 }
1235 }
1236
1237 ///
1238 /// Move constructor (`other` becomes unarmed).
1239 ///
1240 BindingGuard(BindingGuard&& other) noexcept
1241 : api_(other.api_), path_(std::move(other.path_)) {
1242 other.api_ = nullptr;
1243 }
1244
1245 ///
1246 /// Move assignment (removes the current binding first if armed, and `other` becomes unarmed).
1247 ///
1249 if (this != &other) {
1250 if (api_) {
1251 ulJSAPIUnbind(api_, path_.c_str());
1252 ulDestroyJSAPI(api_);
1253 }
1254 api_ = other.api_;
1255 path_ = std::move(other.path_);
1256 other.api_ = nullptr;
1257 }
1258 return *this;
1259 }
1260 BindingGuard(const BindingGuard&) = delete;
1262
1263 ///
1264 /// Disarm the guard without removing the binding (it stays until you unbind it or the API is
1265 /// destroyed).
1266 ///
1267 /// Safe to call from any thread.
1268 ///
1269 void Release() {
1270 if (api_) {
1271 ulDestroyJSAPI(api_);
1272 api_ = nullptr;
1273 }
1274 path_.clear();
1275 }
1276
1277 ///
1278 /// Whether or not the guard is armed (holds a path to remove).
1279 ///
1280 explicit operator bool() const { return api_ != nullptr; }
1281
1282 private:
1283 ULJSAPI api_ = nullptr;
1284 std::string path_;
1285};
1286
1287///
1288/// The details an API injection filter decides on (see SetInjectionFilter()).
1289///
1290/// New members may be added at the end in later versions, so read the members by name.
1291///
1293 ///
1294 /// The View loading the page (the View you set the filter on).
1295 ///
1297
1298 ///
1299 /// The API being decided on. It's the handle your API's raw() returns, so compare the two to
1300 /// tell your APIs apart. Wrap it with API::FromBorrowed() to use it as an API.
1301 ///
1303
1304 ///
1305 /// The page's security origin, serialized (eg, "https://example.com", or "null" for an opaque
1306 /// origin).
1307 ///
1308 const char* origin;
1309
1310 ///
1311 /// Whether or not the page is in the View's main frame.
1312 ///
1314
1315 ///
1316 /// Whether or not the origin rules allow the page.
1317 ///
1319};
1320
1321} // namespace js
1322
1323/// \cond INTERNAL
1324namespace detail {
1325template <>
1326struct InjectionRequestTraits<ULJSAPIInjectionRequest> {
1327 static js::InjectionRequest ToRequest(View* view, const ULJSAPIInjectionRequest& request) {
1328 return { view, request.api, request.origin, request.is_main_frame, request.rules_allow };
1329 }
1330 static void ReportException(const char* what) {
1331 std::string message = "js::SetInjectionFilter: the filter threw a C++ exception, so the "
1332 "JavaScript API was withheld from the page";
1333 if (what) {
1334 message += ": ";
1335 message += what;
1336 }
1337 ulJSAPIEmitDiagnostic(nullptr, message.c_str());
1338 }
1339};
1340} // namespace detail
1341/// \endcond
1342
1343namespace js {
1344
1345///
1346/// Set a View's API filter with a C++ callable (the typed form of
1347/// View::SetJSAPIInjectionFilter()).
1348///
1349/// The View calls the filter each time it's about to add an attached API's bindings to a page:
1350/// when a page loads, when you attach an API, and when a page returns from the back-forward
1351/// cache. The origin rules run first and their verdict arrives in `rules_allow`. Return true to
1352/// add the bindings or false to withhold them, whatever the rules decided. Use it for decisions
1353/// that origin rules can't express, such as a user setting or allowing a page whose origin is
1354/// opaque.
1355///
1356/// ```
1357/// js::SetInjectionFilter(view.get(), [&](const js::InjectionRequest& request) {
1358/// if (request.api == debug_api.raw())
1359/// return settings.debug_tools_enabled && request.rules_allow;
1360/// return request.rules_allow;
1361/// });
1362/// ```
1363///
1364/// @param view The View to set the filter on (nothing happens if it's nullptr).
1365///
1366/// @param filter A callable invocable as `bool(const js::InjectionRequest&)`. The new filter
1367/// replaces (and destroys) the previous one.
1368///
1369/// \parblock
1370/// @note Call this on the Renderer's thread. The filter runs there too.
1371/// \endparblock
1372///
1373/// \parblock
1374/// @note The filter is called for subframes only for APIs attached with AllFrames. The request
1375/// is valid only during the call.
1376/// \endparblock
1377///
1378/// \parblock
1379/// @note A filter that throws a C++ exception withholds the API from that page (and the
1380/// library logs a warning).
1381/// \endparblock
1382///
1383/// @see ClearInjectionFilter()
1384///
1385template <typename F>
1386void SetInjectionFilter(View* view, F&& filter) {
1387 using Fn = std::decay_t<F>;
1388 static_assert(std::is_invocable_r_v<bool, Fn&, const InjectionRequest&>,
1389 "js::SetInjectionFilter takes a callable invocable as "
1390 "bool(const js::InjectionRequest&)");
1391 if (!view)
1392 return;
1393 using Adapter = ultralight::detail::InjectionFilterAdapter<Fn, ULJSAPIInjectionRequest>;
1394 view->SetJSAPIInjectionFilter(&Adapter::Invoke, new Adapter { std::forward<F>(filter), view },
1395 &Adapter::Destroy);
1396}
1397
1398///
1399/// Remove a View's API filter, so the origin rules alone decide which pages get the bindings.
1400///
1401/// @param view The View to clear the filter on (nothing happens if it's nullptr).
1402///
1403/// @note Call this on the Renderer's thread.
1404///
1405inline void ClearInjectionFilter(View* view) {
1406 if (view)
1407 view->SetJSAPIInjectionFilter(nullptr, nullptr, nullptr);
1408}
1409
1410///
1411/// Bind a member function to an instance without writing a lambda.
1412///
1413/// How you pass the instance decides who keeps it alive:
1414///
1415/// ```
1416/// api["save"] = js::Bind(this, &App::OnSave); // raw pointer: you do
1417/// api["query"] = js::Bind(database_, &Database::Query); // shared holder: the binding does
1418/// api["tick"] = js::Bind(std::weak_ptr(widget_), // weak holder: nobody, and calls
1419/// &Widget::OnTick); // fail safely once it's gone
1420/// ```
1421///
1422/// The result is an ordinary callable that you can bind anywhere a lambda works. A method that
1423/// returns js::Task<T> or takes a trailing js::Resolver binds as an async function in every
1424/// form.
1425///
1426/// @param instance The object to call the method on. It must outlive every call through the
1427/// binding.
1428///
1429/// @param method The member function pointer.
1430///
1431/// @return Returns a callable that calls `method` on `instance`.
1432///
1433template <typename T, typename M>
1434 requires std::is_member_function_pointer_v<M>
1435auto Bind(T* instance, M method) {
1436 return detail::MakeBoundMethod(instance, method, detail::MemberFnTraits<M> {});
1437}
1438
1439///
1440/// Bind a member function to an instance kept by a holder.
1441///
1442/// Any smart pointer with an ultralight::HolderTraits specialization works, built-in or your own:
1443///
1444/// - **A shared holder** (RefPtr, std::shared_ptr, js::Eternal, or your own shared
1445/// specialization) is kept by the binding, so the instance lives as long as the binding.
1446/// - **A weak holder** (WeakPtr, std::weak_ptr, or your own specialization with Lock()) is
1447/// locked for each call and never keeps the instance alive between calls.
1448///
1449/// A method that returns js::Task<T> keeps the instance alive until its Task finishes, in both
1450/// forms (the weak form keeps the locked holder), even if the binding is removed meanwhile. A
1451/// method that takes a trailing js::Resolver binds in both forms too, but the weak form keeps
1452/// the instance only during the call, not until the Resolver settles.
1453///
1454/// @param holder The holder to call the method through (copied into the binding).
1455///
1456/// @param method The member function pointer.
1457///
1458/// @return Returns a callable that calls `method` on the held instance.
1459///
1460/// \parblock
1461/// @note When the instance is gone (an empty shared holder, or a weak holder whose object was
1462/// destroyed), a call throws a TypeError with code `ULJS_DETACHED` into the page instead
1463/// of touching the missing object. An async binding rejects its Promise with that error.
1464/// \endparblock
1465///
1466/// \parblock
1467/// @note std::unique_ptr isn't accepted, since passing one would move ownership into the
1468/// binding. Keep ownership and bind `js::Bind(ptr.get(), &T::Method)` instead.
1469/// \endparblock
1470///
1471template <typename H, typename M>
1472 requires((Holder<H> || LockableHolder<H>) && std::is_member_function_pointer_v<M>)
1473auto Bind(H holder, M method) {
1474 using T = typename HolderTraits<H>::element_type;
1475 static_assert(std::is_base_of_v<typename detail::MemberFnTraits<M>::Class, T>,
1476 "the member pointer must belong to the holder's element type (or a base "
1477 "of it)");
1478 if constexpr (Holder<H> && !LockableHolder<H>) {
1480 "js::Bind does not take exclusive holders: passing a unique_ptr would "
1481 "silently move ownership into the binding. Keep ownership and borrow "
1482 "with js::Bind(ptr.get(), &T::Method), or bind a shared holder");
1483 }
1484 return detail::MakeHolderBoundMethod<H, T>(std::move(holder), method,
1485 detail::MemberFnTraits<M> {});
1486}
1487
1488} // namespace js
1489} // namespace ultralight
1490
1491#pragma pop_macro("None")
struct C_JSAPI * ULJSAPI
Opaque handle to a set of native JavaScript bindings.
Definition View.h:32
struct ULJSAPIInjectionRequest ULJSAPIInjectionRequest
The details an API injection filter decides on.
Definition View.h:60
A list of origin patterns that controls which pages get an attached API.
Definition OriginRules.h:90
Web-page container rendered to an offscreen surface.
Definition View.h:483
virtual bool AttachJSAPI(ULJSAPI api, unsigned flags=0, const char *const *origin_rules=nullptr, size_t num_origin_rules=0)=0
Attach a JavaScript API to this View.
virtual void DetachJSAPI(ULJSAPI api)=0
Detach a JavaScript API from this View.
virtual void SetJSAPIInjectionFilter(JSAPIInjectionFilter filter, void *user_data, void(*destroy_user_data)(void *))=0
Set a filter that decides whether an attached API's bindings are added to a page.
The assignable path that operator[] returns (see Binding Functions and Values in the class overview).
Definition API.h:492
Binder(const Binder &)=delete
Binder & operator=(T &&rhs) &&
Bind a callable or value at this path (the type of rhs decides what's bound, see Binding Functions an...
Definition API.h:506
friend class API
Definition API.h:581
Binder & operator=(const Binder &)=delete
Binder operator[](const char *segment) &&
Get the Binder for a child path (api["fs"]["readFile"] is api["fs.readFile"]).
Definition API.h:555
void BindProperty(Getter &&get, Setter &&set=nullptr) &&
Bind a property with a getter (and optional setter) at this path (see API::BindProperty()):
Definition API.h:575
A global JavaScript namespace for native functions and data.
Definition API.h:398
void BindProperty(const char *path, Getter &&get, Setter &&set=nullptr)
Bind a property with a getter and an optional setter.
Definition API.h:811
void DefineEvent(const char *event_path, const Anns &... annotations)
Declare an event and its payload types.
Definition API.h:937
Diagnostics diagnostics() const
Get the effective diagnostics level.
Definition API.h:1081
API & operator=(API &&other) noexcept
Move assignment (releases the current handle, and other becomes empty).
Definition API.h:436
bool Unbind(const char *path)
Remove the binding at a path, along with its metadata.
Definition API.h:980
API(const char *root_path, const APIOptions &options={})
Create an API rooted at a global namespace path.
Definition API.h:412
void set_diagnostics(Diagnostics level)
Set the diagnostics level (see Diagnostics).
Definition API.h:1070
ULJSAPI raw() const
Get the underlying handle, for calls into the C API.
Definition API.h:1145
static API Adopt(ULJSAPI api)
Wrap a handle you own from the C API, taking ownership of it (eg, the result of ulCreateJSAPI()).
Definition API.h:460
ClassBuilder< T > DefineClass(const char *path)
Define a native class at a path, so page script can call its methods, read its properties,...
Definition Class.h:683
void Bind(const char *path, Fn &&fn, const Anns &... annotations)
Bind a callable at a path (the explicit form of api[path] = fn, which also takes annotations).
Definition API.h:613
~API()
Destructor (releases this handle).
Definition API.h:424
bool AttachTo(View *view, const AttachOptions &options={})
Attach this API to a View.
Definition API.h:1119
void Emit(const char *event_path, Args &&... args)
Emit an event to the page listeners subscribed with on or once (see Emitting Events in the class over...
Definition API.h:882
API(API &&other) noexcept
Move constructor (other becomes empty).
Definition API.h:429
API & operator=(const API &)=delete
bool DumpSchema(const char *utf8_path)
Write the JSON from schema() to a file.
Definition API.h:1018
Binder operator[](const char *path)
Get the Binder for a path relative to the root (see Binder).
Definition API.h:594
void SetConstant(const char *path, T &&value)
Set a constant at a path (the explicit form of api[path] = value).
Definition API.h:713
static API FromBorrowed(ULJSAPI api)
Wrap a handle someone else owns, without owning the API (eg, the api member of a js::InjectionRequest...
Definition API.h:476
API(const API &)=delete
void DetachFrom(View *view)
Detach this API from a View.
Definition API.h:1135
ULJSAPI LeakRef()
Give up ownership of the underlying handle without releasing it (this API becomes empty).
Definition API.h:1152
void SetMetadata(const char *path, const char *metadata_json)
Add schema metadata (eg, documentation) to the entry at a path.
Definition API.h:1041
std::string schema() const
Get the API's schema as JSON.
Definition API.h:996
BindingGuard(const BindingGuard &)=delete
BindingGuard()=default
Create an unarmed guard (removes nothing).
BindingGuard(BindingGuard &&other) noexcept
Move constructor (other becomes unarmed).
Definition API.h:1240
BindingGuard & operator=(const BindingGuard &)=delete
void Release()
Disarm the guard without removing the binding (it stays until you unbind it or the API is destroyed).
Definition API.h:1269
BindingGuard(const API &api, const char *path)
Create an armed guard that removes the binding at path when destroyed.
Definition API.h:1224
~BindingGuard()
Destructor (removes the binding if the guard is armed).
Definition API.h:1230
BindingGuard & operator=(BindingGuard &&other) noexcept
Move assignment (removes the current binding first if armed, and other becomes unarmed).
Definition API.h:1248
Builder for exposing a C++ class to JavaScript.
Definition Class.h:277
Whether or not H (ignoring const and references) is an owning holder: its ultralight::HolderTraits de...
Definition Holder.h:102
Whether or not H (ignoring const and references) is a weak holder: its ultralight::HolderTraits decla...
Definition Holder.h:113
Whether or not js::TypeTraits can convert T (ignoring const, volatile, and references).
Definition TypeTraits.h:275
Definition StringSTL.h:166
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
DiagnosticsLevel Diagnostics
The diagnostics levels for an API.
Definition API.h:150
auto Bind(T *instance, M method)
Bind a member function to an instance without writing a lambda.
Definition API.h:1435
void SetInjectionFilter(View *view, F &&filter)
Set a View's API filter with a C++ callable (the typed form of View::SetJSAPIInjectionFilter()).
Definition API.h:1386
constexpr AttachFlags operator|(AttachFlags a, AttachFlags b)
Combine attach flags.
Definition API.h:56
@ ReadOnly
Script can't change its value.
Definition Value.h:74
void ClearInjectionFilter(View *view)
Remove a View's API filter, so the origin rules alone decide which pages get the bindings.
Definition API.h:1405
constexpr AttachFlags & operator|=(AttachFlags &a, AttachFlags b)
Add attach flags to a.
Definition API.h:65
AttachFlags
The flags for API::AttachTo() (the C++ form of ULJSAPIAttachFlags).
Definition API.h:44
@ AllFrames
Also add the bindings to subframes.
Definition API.h:46
@ None
Main frame only, with frozen namespace objects.
Definition API.h:45
@ Mutable
Don't freeze the namespace objects (page script can change or replace the bindings).
Definition API.h:47
constexpr AttachFlags AllFrames
Shorthand for AttachFlags::AllFrames.
Definition API.h:72
constexpr AttachFlags Mutable
Shorthand for AttachFlags::Mutable.
Definition API.h:77
Root namespace for every public Ultralight type, function, and enumeration.
DiagnosticsLevel
How much checking and warning the library does for mistakes made through one of its APIs.
Definition DiagnosticsLevel.h:18
@ None
Definition Anchor.h:36
@ Shared
Copies share ownership (eg, RefPtr), or the instance is never destroyed (js::Eternal).
Definition HolderTraits.h:20
Traits template that lets the library hold objects through custom smart pointers.
Definition HolderTraits.h:156
Options for creating an API.
Definition API.h:159
Diagnostics diagnostics
The diagnostics level.
Definition API.h:166
Options for API::AttachTo().
Definition API.h:91
OriginRules origin_rules
The origin rules for the pages that get the bindings.
Definition API.h:101
AttachFlags flags
The attach flags (see AttachFlags).
Definition API.h:95
const char * text
The documentation text.
Definition API.h:228
constexpr Doc(const char *doc_text)
Create a documentation annotation.
Definition API.h:235
The details an API injection filter decides on (see SetInjectionFilter()).
Definition API.h:1292
const char * origin
The page's security origin, serialized (eg, "https://example.com", or "null" for an opaque origin).
Definition API.h:1308
bool rules_allow
Whether or not the origin rules allow the page.
Definition API.h:1318
bool is_main_frame
Whether or not the page is in the View's main frame.
Definition API.h:1313
ULJSAPI api
The API being decided on.
Definition API.h:1302
View * view
The View loading the page (the View you set the filter on).
Definition API.h:1296
constexpr Param(const char *declared_name)
Create a parameter name annotation.
Definition API.h:210
const char * name
The parameter name.
Definition API.h:203