docs
Loading...
Searching...
No Matches
Class.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#include <Ultralight/CAPI/CAPI_JSClass.h>
7#include <Ultralight/js/API.h>
9#include <Ultralight/js/detail/ClassBinding.h>
10
11#include <algorithm>
12#include <map>
13#include <memory>
14#include <string>
15#include <tuple>
16#include <type_traits>
17#include <utility>
18
19namespace ultralight {
20namespace js {
21
22///
23/// Select one overload of a member function for ClassBuilder::Method().
24///
25/// Write the overload's signature as the template argument:
26///
27/// ```
28/// builder.Method("query", js::Overload<Row(const std::string&)>(&Database::Query));
29/// builder.Method("size", js::Overload<size_t() const>(&Database::Size));
30/// ```
31///
32/// @param member The overloaded member function.
33///
34/// @return Returns the member-function pointer for that overload.
35///
36template <typename Signature, typename C>
37constexpr auto Overload(Signature C::* member) {
38 return member;
39}
40
41///
42/// Converts a pointer to a bound class as a borrowed instance (you keep ownership).
43///
44/// As a return value it gives the page the instance's wrapper without giving it ownership. As a
45/// parameter it gives you the instance for the duration of the call. NULL converts to `null`
46/// and back. Passing a detached instance throws a TypeError with code `ULJS_DETACHED`.
47///
48/// @note API::Emit() and Resolver::Resolve() don't accept raw instance pointers, because they
49/// deliver later. Pass the class's canonical holder instead (see js::ClassHolder).
50///
51/// @see js::ClassBuilder, js::TypeTraits
52///
53template <typename T>
54struct TypeTraits<T*, std::enable_if_t<std::is_class_v<T>>> {
55 static bool FromJS(ULJSContext ctx, ULJSValue value, T** out, ULJSValue* exception) {
56 return detail::UnwrapInstance<T>(ctx, value, out, exception);
57 }
58 static ULJSValue ToJS(ULJSContext ctx, T* const& value) {
59 return detail::WrapView(ctx, value);
60 }
61 static constexpr const char* SchemaType() { return detail::ClassSchemaName<T>(); }
62};
63
64///
65/// Converts a bound class's canonical holder (see js::ClassHolder) with ownership.
66///
67/// Returning the holder makes the instance's wrapper an owner of the instance:
68///
69/// - **Exclusive holders** (std::unique_ptr) move ownership to the wrapper and leave your
70/// holder empty, even when it's passed by const reference. One can't be a parameter, since
71/// the page can't give up ownership through an argument. Take a `T*` instead, or take
72/// ownership back with js::Detach().
73/// - **Shared holders** (RefPtr, std::shared_ptr, js::Eternal) add the wrapper as another
74/// owner. As a parameter you get a holder that shares ownership with the wrapper.
75///
76/// @note A std::shared_ptr (or custom shared holder) parameter only accepts a wrapper that owns
77/// its instance. A wrapper created from a `T*` return throws a TypeError there.
78///
79/// @see js::ClassBuilder, ultralight::HolderTraits
80///
81template <typename H>
82struct TypeTraits<H, std::enable_if_t<detail::kIsCanonicalHolder<H>>> {
83 private:
84 using Traits = HolderTraits<H>;
85 using T = typename Traits::element_type;
86
87 public:
88 static bool FromJS(ULJSContext ctx, ULJSValue value, H* out, ULJSValue* exception) {
89 static_assert(Traits::kind == HolderKind::Shared,
90 "an exclusive holder cannot be a parameter: JavaScript cannot relinquish "
91 "ownership through an argument. Borrow with T*, or take ownership "
92 "explicitly with js::Detach<T>()");
93 T* instance = nullptr;
94 if (!detail::UnwrapInstance<T>(ctx, value, &instance, exception))
95 return false;
96 if (!instance) {
97 *out = H {};
98 return true;
99 }
100 if constexpr (std::is_same_v<H, RefPtr<T>> || std::is_same_v<H, Eternal<T>>) {
101 *out = H(instance); // A new stake is constructible from the instance itself.
102 return true;
103 } else {
104 // The stake must be copied from the wrapper's own holder (it cannot be reconstructed
105 // from the instance pointer), so the wrapper must be bridge-owned. The magic check
106 // keeps a holder attached through the C API from being misread as a typed stake.
107 void* raw = ulJSObjectGetInstanceHolder(value, detail::BoundClass<T>());
108 if (!raw || static_cast<detail::StakeBox<H>*>(raw)->magic != detail::kStakeBoxMagic) {
109 if (exception) {
110 std::string name = detail::ClassSchemaName<T>();
111 std::string message = "this " + name
112 + " has no shared owner this parameter can copy (declare the parameter as "
113 + name + "* to borrow the instance)";
114 *exception = Error::TypeError(message.c_str()).ToJS(ctx);
115 }
116 return false;
117 }
118 *out = static_cast<detail::StakeBox<H>*>(raw)->holder;
119 return true;
120 }
121 }
122
123 static ULJSValue ToJS(ULJSContext ctx, const H& value) {
124 if constexpr (Traits::kind == HolderKind::Exclusive)
125 return detail::WrapOwned<H>(ctx, std::move(const_cast<H&>(value)));
126 else
127 return detail::WrapOwned<H>(ctx, H(value));
128 }
129
130 static constexpr const char* SchemaType() { return detail::ClassSchemaName<T>(); }
131};
132
133///
134/// Builder for exposing a C++ class to JavaScript.
135///
136/// Calling API::DefineClass() returns a ClassBuilder to bind a C++ class under an API namespace,
137/// giving page scripts real objects backed by your C++ instances. Scripts can create instances with
138/// `new` or receive them from bound functions.
139///
140/// Chain member declarations on the builder to define the class:
141///
142/// ```
143/// class Database {
144/// public:
145/// explicit Database(std::string file);
146///
147/// std::string Query(std::string sql);
148/// double timeout = 30;
149///
150/// int page_size() const;
151/// void set_page_size(int bytes);
152/// };
153///
154/// app.DefineClass<Database>("Database")
155/// .Constructor<std::string>()
156/// .Method("query", &Database::Query)
157/// .Field("timeout", &Database::timeout)
158/// .Property("pageSize", &Database::page_size, &Database::set_page_size);
159/// ```
160///
161/// Page scripts can instantiate the class and call its methods:
162///
163/// ```js
164/// const db = new app.Database("save.db");
165/// db.timeout = 60;
166/// const rows = db.query("SELECT * FROM saves");
167///
168/// db.query(42); // TypeError: app.Database.query(string): argument 1:
169/// // expected string, got number
170/// ```
171///
172/// ## Class Registration
173///
174/// The class registers when the ClassBuilder is destroyed, which usually happens at the end of the
175/// statement.
176///
177/// The library registers each C++ class once per process. A later call to API::DefineClass() for
178/// the same C++ type attaches the existing definition to another path and can't add or alter
179/// members.
180///
181/// If you omit Constructor(), calling `new` on the page throws a TypeError with code
182/// `ULJS_NO_CTOR`. This lets you require callers to obtain instances through static factory methods
183/// bound with StaticMethod() or through functions that return an instance.
184///
185/// ## Instance Ownership
186///
187/// An instance crosses to JavaScript through either an owning holder or a borrowed pointer:
188///
189/// - **An owned instance uses the canonical holder.** The holder type is configured by specializing
190/// js::ClassHolder, defaulting to `RefPtr<T>` for RefCounted types and `std::unique_ptr<T>`
191/// otherwise.
192/// - **A raw pointer borrows the instance.** Returning a `T*` gives the page a wrapper without
193/// transferring ownership. You must keep the native object alive while pages can call into it, or
194/// detach its wrapper before freeing it.
195///
196/// Functions bound to an API can return either owned instances or borrowed pointers:
197///
198/// ```
199/// class Enemy {
200/// public:
201/// double health = 100;
202/// };
203///
204/// void RegisterEnemyAPI(js::API& api, Enemy& boss) {
205/// api.DefineClass<Enemy>("Enemy").Field("health", &Enemy::health);
206///
207/// // The page owns each new Enemy (std::unique_ptr is the default holder).
208/// api["spawnEnemy"] = [] { return std::make_unique<Enemy>(); };
209///
210/// // A borrow: the page gets `boss`, and you keep ownership.
211/// api["boss"] = [&boss]() -> Enemy* { return &boss; };
212/// }
213/// ```
214///
215/// The library releases owned instances on the Renderer's thread during a later Renderer::Update()
216/// when their wrapper is garbage-collected or when their page navigates away.
217///
218/// @note Navigating away releases owned instances even when the page enters the back-forward
219/// cache. Calls on those instances after the page is restored throw a TypeError with code
220/// `ULJS_DETACHED`.
221///
222/// ## Closing an Instance
223///
224/// JavaScript garbage collection runs unpredictably. Any class that manages a scarce resource, such
225/// as an open file or network socket, should provide an explicit close method built on
226/// js::Detach().
227///
228/// Calling js::Detach() on a wrapper releases the native instance and returns the canonical holder,
229/// freeing the instance when that holder is destroyed. Later JavaScript calls on that detached
230/// wrapper throw a TypeError with code `ULJS_DETACHED`.
231///
232/// Implement an explicit close method by detaching the wrapper during a method call:
233///
234/// ```
235/// class Connection {
236/// public:
237/// void Disconnect(js::CallInfo info) {
238/// js::Detach<Connection>(info.this_value().ToValue()); // frees this
239/// // Don't touch members after the Detach.
240/// }
241/// };
242///
243/// void RegisterConnection(js::API& api) {
244/// api.DefineClass<Connection>("Connection")
245/// .Constructor<>()
246/// .Method("disconnect", &Connection::Disconnect);
247/// }
248/// ```
249///
250/// Subsequent calls on the disconnected wrapper fail with a TypeError:
251///
252/// ```js
253/// const conn = new app.Connection();
254/// conn.disconnect();
255/// conn.disconnect(); // TypeError with code "ULJS_DETACHED"
256/// ```
257///
258/// Detaching an instance follows specific lifetime rules:
259///
260/// - **Pending async calls delay instance destruction.** If an async method on the instance is
261/// still running when js::Detach() is called, the wrapper detaches immediately but js::Detach()
262/// returns an empty holder. The library keeps the instance alive until that call settles,
263/// releasing it during a later Renderer::Update().
264/// - **Borrowed instances must be detached across all pages.** Because each page creates its own
265/// wrapper for a borrowed instance, you must call js::Detach() on every page's wrapper before
266/// freeing the native object.
267///
268/// ## Ownership Cycles
269///
270/// A native instance that stores a js::Value pointing back to its own JavaScript wrapper creates a
271/// reference cycle that prevents garbage collection while the page is alive. To break the cycle,
272/// reset the stored handle in an explicit close method, or store a js::WeakValue instead.
273///
274/// @see js::API::DefineClass(), js::ClassHolder, js::Detach(), js::Eternal, js::Overload()
275///
276template <typename T>
278 public:
279 ///
280 /// The class's canonical holder type (see js::ClassHolder).
281 ///
283
284 ClassBuilder(ClassBuilder&& other) noexcept
285 : api_(other.api_), cls_(other.cls_), path_(std::move(other.path_)),
286 full_path_(std::move(other.full_path_)), shapes_(std::move(other.shapes_)),
287 ctor_(std::move(other.ctor_)), methods_(std::move(other.methods_)),
288 properties_(std::move(other.properties_)), statics_(std::move(other.statics_)) {
289 other.api_ = nullptr;
290 }
291 ClassBuilder(const ClassBuilder&) = delete;
294
296 if (api_ && cls_ && ulJSAPIRegisterClass(api_, path_.c_str(), cls_))
297 EmitShapeEnrichment();
298 }
299
300 ///
301 /// Declare the constructor's parameter types.
302 ///
303 /// `new` converts its arguments to these types (throwing a TypeError on a mismatch), then
304 /// constructs the instance into the class's canonical holder, owned by the new wrapper.
305 ///
306 /// @return Returns this builder, so calls chain.
307 ///
308 /// @note This works only when the canonical holder is RefPtr, std::unique_ptr, or
309 /// std::shared_ptr. For another holder (js::Eternal included), bind a StaticMethod()
310 /// that returns the holder instead.
311 ///
312 template <typename... Args>
314 (detail::ValidateParam<Args>(), ...);
315 static_assert(std::is_constructible_v<std::remove_cv_t<T>, detail::ParamStorage<Args>&&...>,
316 "T is not constructible from these argument types");
317 if (cls_) {
318 auto* bound = new detail::ClassCtor<T, Args...> { full_path_ };
319 if (ulJSClassSetConstructor(cls_, &detail::ClassCtor<T, Args...>::Invoke, bound,
320 &detail::DestroyPayload<detail::ClassCtor<T, Args...>>))
321 ctor_ = detail::BuildCallableFragment<std::tuple<Args...>, void, true>(shapes_);
322 }
323 return *this;
324 }
325
326 ///
327 /// Add an instance method.
328 ///
329 /// Arguments and the return value convert like those of a function bound with API::Bind().
330 /// A method that returns a js::Task or takes a trailing js::Resolver returns a Promise to the
331 /// page.
332 ///
333 /// @param name The method's name in JavaScript.
334 ///
335 /// @param method A member function of T (or of a base of T). Select one overload of an
336 /// overloaded function with js::Overload().
337 ///
338 /// @param annotations Optional js::Param / js::Doc annotations for the parameter names and
339 /// documentation (as on API::Bind()).
340 ///
341 /// @return Returns this builder, so calls chain.
342 ///
343 template <typename M, typename... Anns>
344 requires(std::is_member_function_pointer_v<M> && (detail::Annotation<Anns> && ...))
345 ClassBuilder& Method(const char* name, M method, const Anns&... annotations) {
346 using Traits = detail::MemberFnTraits<M>;
347 static_assert(std::is_base_of_v<typename Traits::Class, T>,
348 "the member pointer must belong to T (or a base of T)");
349 using Shape = detail::ParamShape<typename Traits::Params>;
350 [&]<size_t... Is>(std::index_sequence<Is...>) {
351 (detail::ValidateParam<std::tuple_element_t<Is, typename Shape::Converted>>(), ...);
352 }(std::make_index_sequence<Shape::kCount> {});
353 constexpr size_t kNames = detail::kArgAnnotationCount<Anns...>;
354 static_assert(kNames == 0 || kNames == Shape::kCount,
355 "js::Param annotations must name every payload parameter or none");
356 static_assert(detail::kDocAnnotationCount<Anns...> <= 1,
357 "at most one js::Doc annotation per registration");
358 detail::AnnotationSet set = detail::CollectAnnotations(annotations...);
359 if (cls_) {
360 auto* bound
361 = new detail::BoundClassMethod<T, M> { method, MemberPath(name), {}, {} };
362 bound->display
363 = bound->path + detail::RenderSignature<typename Shape::Converted>(set);
364 bound->names.assign(set.names, set.names + set.name_count);
365 if (!ulJSClassAddMethod(cls_, name, &detail::ClassMethodTrampoline<T, M>, bound,
366 &detail::DestroyPayload<detail::BoundClassMethod<T, M>>,
367 kULJSPropertyAttributes_DontEnum))
368 return *this;
369 using R = typename Traits::Return;
370 methods_[name ? name : ""]
371 = detail::BuildCallableFragment<typename Shape::Converted, R, Shape::kHasResolver,
372 Shape::kHasResolver || detail::kIsAsyncReturn<R>>(
373 shapes_, set);
374 }
375 return *this;
376 }
377
378 ///
379 /// Expose a data member as a property.
380 ///
381 /// Reads return the member's current value. Assignments convert the value and store it
382 /// (throwing a TypeError on a mismatch). A const member is read-only.
383 ///
384 /// @param name The property's name in JavaScript.
385 ///
386 /// @param member A pointer to a data member of T. For a member T inherits, convert the
387 /// pointer first (eg, `static_cast<int Database::*>(&Base::count)`).
388 ///
389 /// @return Returns this builder, so calls chain.
390 ///
391 template <typename F>
392 ClassBuilder& Field(const char* name, F T::* member) {
393 static_assert(Marshalable<std::remove_cv_t<F>>,
394 "this field type has no js::TypeTraits: use a supported built-in, make it "
395 "a plain aggregate struct, or specialize js::TypeTraits for it");
396 static_assert(!detail::IsCallScopedView<std::remove_cv_t<F>>::value,
397 "a bound field cannot be a std::span (the instance would store a view "
398 "that dies with the bound call); use std::vector<E> or RefPtr<Buffer>");
399 if (cls_) {
400 auto* bound = new detail::BoundField<T, F> { member, MemberPath(name) };
401 bool added;
402 if constexpr (std::is_const_v<F>) {
403 added = ulJSClassAddProperty(cls_, name, &detail::FieldGetterTrampoline<T, F>, nullptr,
404 bound, &detail::DestroyPayload<detail::BoundField<T, F>>);
405 } else {
406 static_assert(detail::ConvertibleFromJS<detail::ParamStorage<F>>,
407 "this field type converts to JavaScript only; expose it through "
408 "Property() with a getter instead");
409 added = ulJSClassAddProperty(cls_, name, &detail::FieldGetterTrampoline<T, F>,
410 &detail::FieldSetterTrampoline<T, F>, bound,
411 &detail::DestroyPayload<detail::BoundField<T, F>>);
412 }
413 if (added) {
414 properties_[name ? name : ""]
415 = detail::BuildTypeFragment<std::remove_cv_t<F>>(shapes_);
416 }
417 }
418 return *this;
419 }
420
421 ///
422 /// Expose a read-only property backed by a getter.
423 ///
424 /// @param name The property's name in JavaScript.
425 ///
426 /// @param getter A member function that takes no parameters and returns a convertible type
427 /// (or a js::Result of one, whose error is thrown).
428 ///
429 /// @return Returns this builder, so calls chain.
430 ///
431 template <typename GM>
432 requires std::is_member_function_pointer_v<GM>
433 ClassBuilder& Property(const char* name, GM getter) {
434 return AddAccessor(name, getter, nullptr);
435 }
436
437 ///
438 /// Expose a read-write property backed by a getter and a setter.
439 ///
440 /// @param name The property's name in JavaScript.
441 ///
442 /// @param getter A member function that takes no parameters and returns a convertible type
443 /// (or a js::Result of one, whose error is thrown).
444 ///
445 /// @param setter A member function that takes one convertible parameter and returns void,
446 /// or a js::Result<void> whose error is thrown to refuse the assignment (eg,
447 /// a value out of range). Any other return type is a compile error.
448 /// Assigning a value of the wrong type throws a TypeError.
449 ///
450 /// @return Returns this builder, so calls chain.
451 ///
452 template <typename GM, typename SM>
453 requires(std::is_member_function_pointer_v<GM> && std::is_member_function_pointer_v<SM>)
454 ClassBuilder& Property(const char* name, GM getter, SM setter) {
455 return AddAccessor(name, getter, setter);
456 }
457
458 ///
459 /// Add a static method (a function on the constructor, eg, `myApp.Database.open(...)`).
460 ///
461 /// Any callable works, with the same conversions and async forms as API::Bind(). A static
462 /// method that returns the canonical holder is the usual way to create instances of a class
463 /// without a Constructor.
464 ///
465 /// @param name The method's name in JavaScript.
466 ///
467 /// @param fn The callable to bind. It must have exactly one non-template call
468 /// signature.
469 ///
470 /// @param annotations Optional js::Param / js::Doc annotations for the parameter names and
471 /// documentation (as on API::Bind()).
472 ///
473 /// @return Returns this builder, so calls chain.
474 ///
475 template <typename Fn, typename... Anns>
476 requires(detail::Annotation<Anns> && ...)
477 ClassBuilder& StaticMethod(const char* name, Fn&& fn, const Anns&... annotations) {
478 using F = std::decay_t<Fn>;
479 static_assert(detail::IsIntrospectableCallable<F>::value,
480 "a static method must have exactly one non-template call signature "
481 "(generic lambdas cannot be introspected)");
482 using Traits = detail::CallableTraits<F>;
483 using Shape = detail::ParamShape<typename Traits::Params>;
484 [&]<size_t... Is>(std::index_sequence<Is...>) {
485 (detail::ValidateParam<std::tuple_element_t<Is, typename Shape::Converted>>(), ...);
486 }(std::make_index_sequence<Shape::kCount> {});
487 constexpr size_t kNames = detail::kArgAnnotationCount<Anns...>;
488 static_assert(kNames == 0 || kNames == Shape::kCount,
489 "js::Param annotations must name every payload parameter or none");
490 static_assert(detail::kDocAnnotationCount<Anns...> <= 1,
491 "at most one js::Doc annotation per registration");
492 detail::AnnotationSet set = detail::CollectAnnotations(annotations...);
493 if (cls_) {
494 auto* bound
495 = new detail::BoundCallable<F> { std::forward<Fn>(fn), MemberPath(name), {}, {} };
496 bound->display
497 = bound->path + detail::RenderSignature<typename Shape::Converted>(set);
498 bound->names.assign(set.names, set.names + set.name_count);
499 if constexpr (detail::HasSettableBoundPath<F>)
500 bound->fn.SetBoundPath(bound->path);
501 if (!ulJSClassAddStaticMethod(cls_, name, &detail::ClassStaticTrampoline<F>, bound,
502 &detail::DestroyBoundCallable<F>))
503 return *this;
504 using R = typename detail::CallableTraits<F>::Return;
505 statics_[name ? name : ""]
506 = detail::BuildCallableFragment<typename Shape::Converted, R, Shape::kHasResolver,
507 Shape::kHasResolver || detail::kIsAsyncReturn<R>>(
508 shapes_, set);
509 }
510 return *this;
511 }
512
513 ///
514 /// Add a static method that calls a member function on an object you own:
515 ///
516 /// ```
517 /// api.DefineClass<Database>("Database")
518 /// .StaticMethod("defaultPath", settings, &Settings::DatabasePath);
519 /// ```
520 ///
521 /// @param name The method's name in JavaScript.
522 ///
523 /// @param receiver The object the method is called on (borrowed, not copied).
524 ///
525 /// @param method A member function of the receiver's class (or of a base of it).
526 ///
527 /// @param annotations Optional js::Param / js::Doc annotations for the parameter names and
528 /// documentation (as on API::Bind()).
529 ///
530 /// @return Returns this builder, so calls chain.
531 ///
532 /// @warning The receiver must stay alive for as long as any page can call the method, even
533 /// after API::Unbind() (class definitions are never removed). Pass a holder
534 /// instead (see the overload below) when you can't guarantee that.
535 ///
536 template <typename C, typename M, typename... Anns>
537 requires(std::is_member_function_pointer_v<M> && !js::Holder<C>
538 && !js::LockableHolder<C> && (detail::Annotation<Anns> && ...))
539 ClassBuilder& StaticMethod(const char* name, C& receiver, M method,
540 const Anns&... annotations) {
541 static_assert(std::is_base_of_v<typename detail::MemberFnTraits<M>::Class, C>,
542 "the member pointer must belong to the receiver's class (or a base of "
543 "it)");
544 return StaticMethod(name, js::Bind(std::addressof(receiver), method), annotations...);
545 }
546
547 ///
548 /// Add a static method that calls a member function through a holder (see js::Bind()).
549 ///
550 /// A shared holder keeps the receiver alive. A weak holder is locked for each call, and a
551 /// call after the receiver is gone throws a TypeError with code `ULJS_DETACHED`.
552 ///
553 /// @param name The method's name in JavaScript.
554 ///
555 /// @param holder A shared or weak holder of the receiver (std::unique_ptr isn't
556 /// accepted).
557 ///
558 /// @param method A member function of the holder's element type (or of a base of it).
559 ///
560 /// @param annotations Optional js::Param / js::Doc annotations for the parameter names and
561 /// documentation (as on API::Bind()).
562 ///
563 /// @return Returns this builder, so calls chain.
564 ///
565 template <typename H, typename M, typename... Anns>
567 && std::is_member_function_pointer_v<M> && (detail::Annotation<Anns> && ...))
568 ClassBuilder& StaticMethod(const char* name, H holder, M method,
569 const Anns&... annotations) {
570 return StaticMethod(name, js::Bind(std::move(holder), method), annotations...);
571 }
572
573 private:
574 friend class API;
575
576 ClassBuilder(ULJSAPI api, ULJSClass cls, std::string full_path, std::string path)
577 : api_(api), cls_(cls), path_(std::move(path)), full_path_(std::move(full_path)) {}
578
579 std::string MemberPath(const char* name) const {
580 return full_path_ + "." + (name ? name : "");
581 }
582
583 template <typename GM, typename SM>
584 ClassBuilder& AddAccessor(const char* name, GM getter, SM setter) {
585 using GTraits = detail::MemberFnTraits<GM>;
586 static_assert(std::tuple_size_v<typename GTraits::Params> == 0,
587 "a property getter takes no parameters");
588 static_assert(!std::is_void_v<typename GTraits::Return>,
589 "a property getter must return a value");
590 if (!cls_)
591 return *this;
592 bool added;
593 if constexpr (std::is_same_v<SM, std::nullptr_t>) {
594 using Bound = detail::BoundAccessor<T, GM, std::nullptr_t>;
595 auto* bound = new Bound { getter, nullptr, MemberPath(name) };
596 added = ulJSClassAddProperty(cls_, name, &detail::AccessorGetterTrampoline<T, Bound>,
597 nullptr, bound, &detail::DestroyPayload<Bound>);
598 } else {
599 using STraits = detail::MemberFnTraits<SM>;
600 static_assert(std::tuple_size_v<typename STraits::Params> == 1,
601 "a property setter takes exactly one parameter");
602 using Bound = detail::BoundAccessor<T, GM, SM>;
603 auto* bound = new Bound { getter, setter, MemberPath(name) };
604 added = ulJSClassAddProperty(cls_, name, &detail::AccessorGetterTrampoline<T, Bound>,
605 &detail::AccessorSetterTrampoline<T, Bound>, bound,
606 &detail::DestroyPayload<Bound>);
607 }
608 if (added) {
609 properties_[name ? name : ""]
610 = detail::BuildTypeFragment<typename GTraits::Return>(shapes_);
611 }
612 return *this;
613 }
614
615 // Emits one root fragment enriching this class's `types` entry with the typed member
616 // signatures (the generated shape is skeletal: member names and readonly flags only),
617 // plus any named shapes those signatures referenced. Fragments accumulate, so a later
618 // DefineClass for an already-defined class that registers no members emits nothing.
619 void EmitShapeEnrichment() {
620 if (ctor_.empty() && methods_.empty() && properties_.empty() && statics_.empty())
621 return;
622 std::string shape_name = path_;
623 std::replace(shape_name.begin(), shape_name.end(), '.', '_');
624 std::string body = "{";
625 bool first = true;
626 if (!ctor_.empty()) {
627 body += "\"ctor\":";
628 body += ctor_;
629 first = false;
630 }
631 auto add_section = [&](const char* key, const std::map<std::string, std::string>& members) {
632 if (members.empty())
633 return;
634 if (!first)
635 body += ',';
636 first = false;
637 body += '"';
638 body += key;
639 body += "\":{";
640 bool member_first = true;
641 for (const auto& [name, entry] : members) {
642 if (!member_first)
643 body += ',';
644 member_first = false;
645 detail::WriteJsonEscaped(body, name);
646 body += ':';
647 body += entry;
648 }
649 body += '}';
650 };
651 add_section("methods", methods_);
652 add_section("properties", properties_);
653 add_section("statics", statics_);
654 body += '}';
655 std::string fragment = "{\"types\":{";
656 detail::WriteJsonEscaped(fragment, shape_name);
657 fragment += ':';
658 fragment += body;
659 for (const auto& [name, shape] : shapes_.shapes) {
660 if (name == shape_name)
661 continue; // A member type sharing the class's shape name would duplicate the key.
662 fragment += ',';
663 detail::WriteJsonEscaped(fragment, name);
664 fragment += ':';
665 fragment += shape;
666 }
667 fragment += "}}";
668 ulJSAPISetMetadata(api_, "", fragment.c_str());
669 }
670
671 ULJSAPI api_ = nullptr;
672 ULJSClass cls_ = nullptr;
673 std::string path_;
674 std::string full_path_;
675 detail::SchemaShapes shapes_;
676 std::string ctor_;
677 std::map<std::string, std::string> methods_;
678 std::map<std::string, std::string> properties_;
679 std::map<std::string, std::string> statics_;
680};
681
682template <typename T>
684 static_assert(std::is_class_v<T>, "DefineClass binds class types");
685 using Bare = std::remove_cv_t<T>;
686 if (!api_ || !path || !*path)
687 return ClassBuilder<T>(nullptr, nullptr, std::string(), std::string());
688 ULJSClass& cls = detail::ClassRegistry<Bare>::cls;
689 if (!cls) {
690 // The class name is the last path segment ("db.Database" names the class "Database").
691 const char* name = path;
692 for (const char* p = path; *p; ++p) {
693 if (*p == '.')
694 name = p + 1;
695 }
696 cls = ulCreateJSClass(name);
697 }
698 return ClassBuilder<T>(api_, cls, FullPath(path), path);
699}
700
701///
702/// Detach a bound-class instance from its wrapper and take ownership back.
703///
704/// After this, calls on the wrapper throw a TypeError with code `ULJS_DETACHED` instead of
705/// reaching the instance. Use it to write close methods (see "Closing an Instance" in
706/// js::ClassBuilder), and to detach a borrowed instance's wrapper before you free the instance.
707///
708/// Each page has its own wrapper for an instance, and this detaches only the one you pass. To
709/// get an instance's wrapper on a page, convert its pointer there (`js::Detach<T>(ctx.Make(ptr))`),
710/// so before you free a borrowed instance, detach its wrapper on every page you gave it to.
711///
712/// @param object The instance's wrapper.
713///
714/// @return Returns the instance's canonical holder. Returns an empty holder if the wrapper
715/// didn't own the instance (it came from a `T*`, so the instance is already yours),
716/// was already detached, or isn't a wrapper of T. Also returns an empty holder while an
717/// async method call on the instance is pending: the wrapper is still detached, and
718/// the instance is released after that call's Promise settles (see "Closing an
719/// Instance" in js::ClassBuilder).
720///
721/// @note Call this on the Renderer's thread.
722///
723template <typename T>
724typename ClassHolder<std::remove_cv_t<T>>::type Detach(const Value& object) {
725 using Bare = std::remove_cv_t<T>;
726 using H = typename ClassHolder<Bare>::type;
727 ULJSClass cls = detail::ClassRegistry<Bare>::cls;
728 if (!cls)
729 return H {};
730 // Peek before detaching: a stake owned by the C holder channel is not a StakeBox, and
731 // refusing here (rather than after the detach) leaves the wrapper attached and its
732 // ownership intact.
733 void* peek = ulJSObjectGetInstanceHolder(object.raw(), cls);
734 if (peek && static_cast<detail::StakeBox<H>*>(peek)->magic != detail::kStakeBoxMagic)
735 return H {};
736 void* holder = nullptr;
737 ulJSObjectDetachInstance(object.raw(), cls, &holder);
738 if (!holder)
739 return H {};
740 auto* box = static_cast<detail::StakeBox<H>*>(holder);
741 H result = std::move(box->holder);
742 delete box;
743 return result;
744}
745
746} // namespace js
747} // namespace ultralight
struct C_JSContext * ULJSContext
Opaque handle to a JavaScript execution context (one page's script world in one View).
Definition View.h:25
struct C_JSAPI * ULJSAPI
Opaque handle to a set of native JavaScript bindings.
Definition View.h:32
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
Builder for exposing a C++ class to JavaScript.
Definition Class.h:277
~ClassBuilder()
Definition Class.h:295
ClassBuilder & Property(const char *name, GM getter)
Expose a read-only property backed by a getter.
Definition Class.h:433
ClassBuilder & Property(const char *name, GM getter, SM setter)
Expose a read-write property backed by a getter and a setter.
Definition Class.h:454
friend class API
Definition Class.h:574
ClassBuilder(ClassBuilder &&other) noexcept
Definition Class.h:284
ClassBuilder & Field(const char *name, F T::*member)
Expose a data member as a property.
Definition Class.h:392
ClassBuilder & operator=(const ClassBuilder &)=delete
ClassBuilder & Constructor()
Declare the constructor's parameter types.
Definition Class.h:313
ClassBuilder(const ClassBuilder &)=delete
typename ClassHolder< std::remove_cv_t< T > >::type HolderType
The class's canonical holder type (see js::ClassHolder).
Definition Class.h:282
ClassBuilder & Method(const char *name, M method, const Anns &... annotations)
Add an instance method.
Definition Class.h:345
ClassBuilder & StaticMethod(const char *name, Fn &&fn, const Anns &... annotations)
Add a static method (a function on the constructor, eg, myApp.Database.open(...)).
Definition Class.h:477
ClassBuilder & operator=(ClassBuilder &&)=delete
ULJSValue ToJS(ULJSContext ctx) const
Convert this error to a JavaScript value.
Definition Error.h:388
static Error TypeError(std::string message)
Create a native TypeError.
Definition Error.h:144
A handle to a live JavaScript value.
Definition Value.h:210
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
js::Value ToJS(const js::Context &context, const Element &element)
Get an element's JavaScript object (the same object the page's scripts see for it).
Definition JSInterop.h:97
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
auto Bind(T *instance, M method)
Bind a member function to an instance without writing a lambda.
Definition API.h:1435
constexpr auto Overload(Signature C::*member)
Select one overload of a member function for ClassBuilder::Method().
Definition Class.h:37
ClassHolder< std::remove_cv_t< T > >::type Detach(const Value &object)
Detach a bound-class instance from its wrapper and take ownership back.
Definition Class.h:724
Root namespace for every public Ultralight type, function, and enumeration.
@ Exclusive
One holder owns the instance and ownership moves (eg, std::unique_ptr).
Definition HolderTraits.h:26
@ 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
Type trait that selects the smart pointer type for owned instances of a bound class.
Definition Holder.h:170
std::conditional_t< std::is_base_of_v< RefCounted, T >, RefPtr< T >, std::unique_ptr< T > > type
Definition Holder.h:171
static ULJSValue ToJS(ULJSContext ctx, T *const &value)
Definition Class.h:58
static constexpr const char * SchemaType()
Definition Class.h:61
static bool FromJS(ULJSContext ctx, ULJSValue value, T **out, ULJSValue *exception)
Definition Class.h:55
Type conversions between C++ and JavaScript across the bridge.
Definition TypeTraits.h:240