You can expose native C++ classes to JavaScript so page script can call into your application using real objects.

Page script can instantiate your class with `new` and call prototype methods like any JavaScript class— object identity is preserved across calls.

You control who owns each instance, choosing between borrowed pointers and smart pointers.

## Defining a Class

Call `api.DefineClass<T>(path)` to expose a native class at that path on the API namespace. The call returns a [`js::ClassBuilder`](/api/cpp/2_0_0/classultralight_1_1js_1_1_class_builder.html) so you can chain member declarations on it:

```cpp
class Database {
 public:
  explicit Database(std::string path);

  std::string Query(std::string sql);
  double timeout = 30;

  std::string path() const;
  void set_path(std::string p);
};

void RegisterDatabase(js::API& api) {
  api.DefineClass<Database>("Database")
     .Constructor<std::string>()
     .Method("query", &Database::Query)
     .Field("timeout", &Database::timeout)
     .Property("path", &Database::path, &Database::set_path);
}
```

The class registers when the builder goes out of scope— normally at the end of the statement.

### Builder Methods

The builder provides methods to declare constructors, prototype methods, and properties:

| Builder Call | What the Page Sees |
|---|---|
| `Constructor<Args...>()` | A constructor callable with `new` that converts arguments to C++ types. |
| `Method(name, &T::fn)` | A method on the prototype. It can run asynchronously by returning a `js::Task` or taking a trailing `js::Resolver`. |
| `Field(name, &T::member)` | A read-write property on each instance, or read-only for const members. |
| `Property(name, getter, setter)` | A property backed by getter and setter accessors. It is read-only when the setter is omitted. |
| `StaticMethod(name, fn)` | A function on the constructor object itself. |
| `js::Overload<Sig>(&T::fn)` | Selects a specific member function overload to pass to `Method()`. |

### Argument Types

Member types are inferred directly from member pointers. A member type with no bridge conversion causes a compile error.

When the page passes an argument of the wrong type, the call throws a TypeError and native code never runs.

### What the Page Sees

When JavaScript interacts with a bound class, the engine wraps each native instance in a standard JavaScript object:

- **Object wrapper** — each native instance has a single wrapper object, so identity comparisons (`===`) hold across the page.
- **Prototype methods** — methods live on the prototype shared by all instances of the class.
- **Instance properties** — fields and properties become own properties of each wrapper.
- **Type checks** — `instanceof` evaluates to `true`, and the constructor's `name` property matches the last segment of the path.
- **Receiver safety** — calling a method with an invalid `this` object or on a detached instance throws a TypeError (code `ULJS_NOT_INSTANCE` or `ULJS_DETACHED`) and never reaches your C++ code.

Page script can create an instance, update fields, and call methods:

```js
const db = new myApp.Database("save.db");
db.timeout = 60;
const rows = db.query("SELECT * FROM saves");
console.log(db instanceof myApp.Database);  // true

db.query(42);
// TypeError: myApp.Database.query(string): argument 1: expected string,
//            got number
```

### Static Methods and Classes Without a Constructor

`StaticMethod()` binds a function directly to the constructor function (such as `myApp.SaveFile.open()`).

If you omit `Constructor()`, calling `new` on the page throws a TypeError (code `ULJS_NO_CTOR`). This lets you require callers to create instances through static factory methods that return a holder.

To call a member function on an object you own, pass the receiver instance to `StaticMethod()`.

> 🚧 Receiver Lifetime
>
> The receiver object must outlive every page because class definitions are never unloaded. If you cannot guarantee its lifetime, pass a shared holder to keep the receiver alive. You can also pass a weak holder, which throws `ULJS_DETACHED` once the receiver is gone.

This example registers a class without a constructor, using static factory methods to create instances:

```cpp
class SaveFile {
 public:
  static std::unique_ptr<SaveFile> Open(std::string path);
  std::string name() const;
};

void RegisterSaveFile(js::API& api, Settings& settings) {
  api.DefineClass<SaveFile>("SaveFile")  // no Constructor: `new` throws
     .StaticMethod("open", &SaveFile::Open)
     .Property("name", &SaveFile::name)
     .StaticMethod("defaultPath", settings, &Settings::SavePath);
}
```

Page script calls the static methods on the constructor to open a file and read its properties:

```js
const save = myApp.SaveFile.open(myApp.SaveFile.defaultPath());
console.log(save.name);
```

### One Class per Type

> 📘 Class Definitions Are Program-Wide
>
> Ultralight registers each C++ class once per process. A later call to `DefineClass()` for the same type attaches the existing class definition to a new path and cannot add or modify members. Calling `Unbind()` removes that path from the registry, but the class definition itself remains.

## Who Owns an Instance

An instance crosses to the page as a reference to the native object. The C++ type passed decides whether the page shares ownership, takes ownership, or borrows the object.

| C++ Type | Ownership | When Destroyed |
|---|---|---|
| `T*` | Borrowed. Native code keeps ownership. | By native code. Keep it alive while pages can use it, or detach its wrapper before freeing it. |
| `RefPtr<T>` / `std::shared_ptr<T>` | Shared between native code and the wrapper. | When the last owner releases it (the wrapper is collected, its page goes away, or `js::Detach()` runs). |
| `std::unique_ptr<T>` | Moved to the wrapper, leaving the native pointer empty. Cannot be a parameter of an exposed C++ function. | When the wrapper is collected or its page goes away. |
| `js::Eternal<T>` | Never owned. | Never. |
| `WeakPtr<T>` / `std::weak_ptr<T>` with `js::Bind()` | Locked into a strong pointer for each call (for a `js::Task`, until the task finishes). | By native code. Calls after the object is destroyed throw a TypeError with code `ULJS_DETACHED`. |

An exposed C++ function can return an owned instance or a borrowed pointer:

```cpp
class Enemy {
 public:
  std::string name;
};

void RegisterEnemyAPI(js::API& api, Enemy& boss) {
  api.DefineClass<Enemy>("Enemy").Field("name", &Enemy::name);

  // The page owns each new Enemy (std::unique_ptr is the default holder).
  api["spawnEnemy"] = [] { return std::make_unique<Enemy>(); };

  // A borrow: the page sees `boss`, and you keep ownership.
  api["boss"] = [&boss]() -> Enemy* { return &boss; };
}
```

> 🚧 Lifetime of Borrowed Instances
>
> A raw `T*` returned to the page is borrowed— the object must outlive every page that can use it. Otherwise, native code must call `js::Detach()` on the instance before freeing it.

### Canonical Holders

Every class has a canonical holder— the smart pointer type its owned instances use to cross to the page. By default, it is `RefPtr<T>` if `T` derives from `RefCounted`, or `std::unique_ptr<T>` otherwise.

Passing an instance to `API::Emit()` or `Resolver::Resolve()` requires the canonical holder. A raw `T*` cannot cross to the page for a later delivery.

The library releases the owned instances it holds on the Renderer's thread during a later `Renderer::Update()`.

> 🚧 Navigation Releases Owned Instances
>
> Navigating away from a page releases the instances it owns, including pages kept in the back-forward cache. When a user restores that page, JavaScript calls on those instances throw a TypeError with code `ULJS_DETACHED`.

#### Choosing Another Holder

To select a different smart pointer, specialize `js::ClassHolder<T>` before defining the class:

```cpp
template <> struct ultralight::js::ClassHolder<Database> {
  using type = std::shared_ptr<Database>;
};
```

`Constructor()` works only when the canonical holder is `RefPtr<T>`, `std::unique_ptr<T>`, or `std::shared_ptr<T>`.

#### Custom Smart Pointers

To use a custom smart pointer (eg, an engine type) as a holder, you'll need two specializations:

1. Specialize [`HolderTraits`](/api/cpp/2_0_0/structultralight_1_1_holder_traits.html) in namespace `ultralight` so the library can reach the underlying object.
2. Specialize `js::ClassHolder` for the class as shown above.

### Program-Lifetime Objects

For singletons and engine subsystems that should never be destroyed, set `js::Eternal<T>` as the canonical holder.

Omit `Constructor()` so page script cannot create instances, and expose the singleton instance as a property on your API namespace.

Specialize `js::ClassHolder` with `js::Eternal` and bind the singleton instance to a property:

```cpp
class GameWorld {
 public:
  static GameWorld& instance();
};

template <> struct ultralight::js::ClassHolder<GameWorld> {
  using type = js::Eternal<GameWorld>;
};

void RegisterWorld(js::API& api) {
  api.DefineClass<GameWorld>("GameWorld");  // no Constructor: `new` throws
  api["world"] = js::Eternal(&GameWorld::instance());
}
```

## Closing an Instance Early

JavaScript garbage collection runs at unpredictable times. Any class holding a scarce resource (such as an open file or a network socket) should provide an explicit close method built on `js::Detach()`.

Calling `js::Detach<T>(wrapper)` releases the native instance from its wrapper and returns the canonical holder— destroying that holder frees the instance. Later JavaScript calls on that detached wrapper throw a TypeError with code `ULJS_DETACHED`.

You should also detach borrowed (`T*`) instances before native code frees them. Because each page has its own wrapper, detach the wrapper on every page the instance was given to.

This close method detaches the wrapper during a method call:

```cpp
class Connection {
 public:
  void Close(js::CallInfo info) {
    js::Detach<Connection>(info.this_value().ToValue());  // frees this
    // Don't touch members after the Detach.
  }
};

void RegisterConnection(js::API& api) {
  api.DefineClass<Connection>("Connection")
     .Constructor<>()
     .Method("close", &Connection::Close);
}
```

Calls on the wrapper throw after it is closed:

```js
const conn = new myApp.Connection();
conn.close();
conn.close();  // TypeError with code "ULJS_DETACHED"
```

### Pending Async Calls

If a call to one of this instance's async methods is still pending (a method returning `js::Task` or taking a trailing `js::Resolver`), `js::Detach()` detaches the wrapper but returns an empty holder. This applies only to async methods on the instance itself, not to other exposed C++ functions.

The instance stays alive until that call's Promise settles. The library then releases it on the Renderer's thread during a later `Renderer::Update()`.

### Ownership Cycles

A native instance that stores a `js::Value` referencing its own wrapper (or a page callback that holds the wrapper) is never garbage-collected until the page unloads.

To release the instance sooner, use one of these approaches:

- **Clear the value**— reset the stored `js::Value` in an explicit close method.
- **Store a weak value**— store a `js::WeakValue` instead when the page keeps the object alive on its own.

> 🚧 Avoid Weak Callbacks
>
> Never use a `js::WeakValue` for a page callback that only native code holds. The engine garbage-collects the callback because nothing on the page keeps it alive.
