- name
- path-dictionary
- description
- Using the OVX path dictionary — the shared interning layer for tokens, prim paths, and prim-path lists across OV libraries. Use when user asks about interning strings/paths, ovx_token_t / ovx_primpath_t / ovx_primpath_list_t, or sharing paths with ovstage.
- license
- LicenseRef-NvidiaProprietary
- version
- 0.1.0
- author
- NVIDIA ovstage
- tags
- ["ovstage","ovx","path-dictionary"]
- tools
- ["Read","Grep"]
# Path Dictionary
## When to Use
Use this skill when the user asks about interning strings or prim paths, working with
`ovx_token_t` / `ovx_primpath_t` / `ovx_primpath_list_t`, building a path list to query
ovstage, or sharing interned paths/tokens across OV libraries.
## Inputs
Resolve inputs in this order: existing repository files and referenced snippets, explicit user request, then broader agent context.
- Whether the caller needs a token (attribute/name interning), a prim path, or a prim-path
list (to query/read a set of prims).
- The dictionary is owned by a producing subsystem; for ovstage, obtain it via
`ovstage_get_path_dictionary` for zero-conversion sharing and never free it (there is no
app-side create/destroy in the public API).
- Whether a path list was created by the caller (`path_dictionary_create_path_list_from_*`
returns it with refcount=1 — pair it with one `path_dictionary_release_path_list_reference`)
or borrowed from an ovstage read result (release only references you added, never ovstage's).
- Repository source snippets referenced below. Treat these snippets as the API source of truth.
## Prerequisites
- Use an ovstage checkout with the path-dictionary headers under `include/ovx/path_dictionary/`
— `path_dictionary.h` (which pulls in `path_dictionary_types.h` and the
`path_dictionary_utils.h` inline wrappers).
- Read the relevant `> **Source:**` snippet before writing or explaining API usage.
- Know the refcount rule for any `ovx_primpath_list_t` before releasing a reference to it.
## Instructions
1. Identify whether the task needs a token, a path, or a path list.
2. Obtain a dictionary from its owner: for ovstage, `ovstage_get_path_dictionary(instance)`
returns a `path_dictionary_instance_t*` (do not free its `vtable`/`context`). All calls
below are the inline wrappers from `path_dictionary_utils.h` and take that instance.
3. Intern strings with `path_dictionary_create_tokens_from_strings` (tokens) and
`path_dictionary_create_paths_from_strings` / `..._create_paths_from_tokens` (prim paths).
Component tokens name an absolute path: omit separators from the tokens,
and expect the same handle as the equivalent absolute string.
Zero components identify `/`; still supply a non-null token array even when all
per-path counts are zero.
Resolve tokens back with `path_dictionary_get_strings_from_tokens`; decompose a prim path
into its tokens with `path_dictionary_get_tokens_from_paths`, then resolve those tokens.
4. Build path lists with `path_dictionary_create_path_list_from_paths` (from interned paths) or
`..._create_path_list_from_strings`; read them back with
`path_dictionary_get_paths_from_path_list` / `..._get_num_paths_from_path_list`.
5. Path lists are refcounted: `path_dictionary_create_path_list_from_*` returns refcount=1
owned by you, so pair it with exactly one `path_dictionary_release_path_list_reference`
(which frees the list when the count reaches zero). To keep a list borrowed from an ovstage
read result, call `path_dictionary_add_path_list_reference` first and release that added
reference when done; never release ovstage's own reference.
6. Check every `ovx_api_result_t::status` against `OVX_API_SUCCESS`; on `OVX_API_ERROR` the
`result.error` (an `ovx_string_t`) describes the failure and must be freed with
`path_dictionary_release_error`.
7. When changing code, run the path-dictionary unit test whenever practical.
## Output Format
- For explanations, cite the relevant API names, source snippets, and caveats.
- For code changes, summarize the files changed, snippets affected, and validation run.
## Scripts
This skill has no scripts.
## Limitations
- The referenced snippets remain the source of truth; update or add tested snippets before documenting new API usage.
- **Python:** covered in the Python section (`ovstage.PathDictionary`; `int` handles,
`str` names, `OvxError`). Inline Python pending a shipping Python example to snippet-source.
- **Snippets** are sourced from the shipping example `examples/c/minimal/main.cpp`.
## Overview
The OVX path dictionary (`<ovx/path_dictionary/path_dictionary.h>`) is the **shared,
zero-cost interning layer** for OV libraries (ovstage, ovrtx, ovphysx, …). The public
surface is a vtable (`path_dictionary_vtable_t`) plus inline wrappers in
`path_dictionary_utils.h`; an instance (`path_dictionary_instance_t`) is produced by an
owner subsystem (e.g. ovstage) and maps strings to stable, trivially comparable handles:
- `ovx_token_t` — interned string (e.g. an attribute name). `OVX_INVALID_TOKEN` is `0`.
- `ovx_primpath_t` — interned prim path. `OVX_INVALID_PRIMPATH` is `0`.
- `ovx_primpath_list_t` — an immutable set/ordering of prim paths. `OVX_INVALID_PRIMPATH_LIST` is `0`.
Design guarantees:
- **Handle identity:** same string → same handle, so consumers use handle equality for
O(1) identity checks (no string compare).
- **Immutable lists:** once created, an `ovx_primpath_list_t` never changes; equal handles
imply identical prim set + ordering (usable as a cache key).
- **Thread-safe interning:** concurrent interning is supported; returned handles and
string pointers are stable for the dictionary's lifetime.
This is the type backbone for `ovx_string_or_token_t` dual-mode arguments throughout
ovstage — see the `string-handling` skill.
## Ownership & lifetime
- The dictionary is **owned by its producing subsystem** (for ovstage, obtained via
`ovstage_get_path_dictionary`). Callers MUST NOT free its `vtable`/`context`; when the
owner reports it gone, every handle minted through it is invalidated simultaneously.
- Tokens and prim paths are **dict-lifetime** (interned, never freed individually); they stay
valid for as long as the dictionary lives.
- String pointers from `path_dictionary_get_strings_from_tokens` point into dictionary-owned
storage and are valid for the dictionary's lifetime — do not free them; copy if you need
them longer. (Error strings inside `ovx_api_result_t` are different — free those with
`path_dictionary_release_error`.)
- `ovx_primpath_list_t` handles are **explicitly refcounted**.
`path_dictionary_create_path_list_from_*` returns a list with **refcount=1** owned by the
caller; `path_dictionary_add_path_list_reference` increments and
`path_dictionary_release_path_list_reference` decrements, freeing the list at zero. Every
`create`/`add` must be paired with exactly one release.
- A list **returned by an ovstage read result** is a **borrow** held by the producing op. Do
not release ovstage's reference; to keep the list beyond the producing handle, call
`path_dictionary_add_path_list_reference` first and release that added reference when finished.
- In Python, `create_path_list*` returns an `ovstage.PathList` that owns its reference and
releases it on `with`-exit or via a finalizer; see "Path-list ownership in Python" below.
The C contract is unchanged — C callers pair every create/add with exactly one release.
## C — intern and resolve
Obtain a dictionary, intern an attribute token and prim paths, build a path list, and
resolve back to strings:
> **Source:** `examples/c/minimal/main.cpp` snippet `intern-and-resolve`
## C — build a path list and query ovstage
The basic stage test interns paths into a list and passes it to
`ovstage_query_from_path_list`, then uses the interned attribute token as the
`ovx_string_or_token_t` argument:
> **Source:** `examples/c/minimal/main.cpp` snippet `path-list-query`
## Sharing with ovstage
For zero-conversion sharing, obtain ovstage's own dictionary instead of creating a
separate one, so tokens/paths interned by the application and by ovstage are directly
comparable:
```c
/* path_dictionary_instance_t* dict = ovstage_get_path_dictionary(instance);
returns ovstage's dictionary (NULL if instance is NULL); valid for the
instance lifetime. Do not free dict->vtable / dict->context. */
```
## Python
The path dictionary is `ovstage.PathDictionary` — a context manager wrapping the same
interning service. Tokens and prim paths are Python `int` handles; strings are `str`. Errors
raise `ovstage.OvxError`.
> **Source:** `examples/python/minimal/main.py` snippet `intern-and-resolve`
>
> Followed by: `examples/python/minimal/main.py` snippet `path-list-query`
Resolve a path list back to strings with `paths.get_path_strings(list)`.
Use `PathDictionary.intern_path` to construct an individual path from a string;
there is no public Python `create_paths_from_tokens` method. Absolute strings
share handle identity with equivalent C component-token paths in the same
dictionary. The root string `/` corresponds to zero component tokens in C.
Construct `PathDictionary(stage)` with the Stage that will use the lists.
Each Stage owns its dictionary; recreate lists in a destination Stage's dictionary
when transferring a selection. List references do not extend Stage lifetime. Same ownership rule as C: release
**caller-created** lists; do not release lists handed back by an ovstage read result.
### Path-list ownership in Python
`create_path_list` / `create_path_list_from_strings` return an `ovstage.PathList`. It
subclasses `int` and **is** the handle — it passes into `query_from_path_list` and every
other slot unchanged, and `isinstance(handle, int)` still holds — but it also owns the
reference the create call minted, and releases it on `with`-exit:
> **Source:** `tests/python/test_path_lists.py` snippet `path-list-context-manager`
`plist.release()` and `paths.destroy_path_list(plist)` are equivalent explicit forms. A
`PathList` whose reference is still outstanding when it is garbage-collected releases it and
emits a `ResourceWarning` — a bug report, not a strategy.
Only `create_path_list*` mints a `PathList`. Borrowed lists from read results stay plain
`int`, so the finalizer can never release a reference the caller does not own.
A `PathList` owns exactly one reference: the one `create_path_list*` minted. A reference you
add with `add_path_list_reference` is yours — pair it with one extra `destroy_path_list`, and
note that dropping the `PathList` never reclaims it, so a bare handle you hand to a
longer-lived consumer keeps working. Passing the `PathList` to `destroy_path_list` releases
its own reference; passing the plain `int` releases one of your added references while any
are outstanding.
`query_from_path_list` wraps a **caller-owned** list, so keep your list alive for as long as
the query and release it yourself. The returned `Query` holds a reference so the list cannot
be finalized under a live query, but that is a safety net, not a transfer of ownership —
releasing the query does not release your list. Bind the list rather than passing a freshly
created one inline: an inline list leaves no handle to release, so it is reclaimed by the
finalizer with a `ResourceWarning`.
### Per-frame loops: create once, reuse
Recreating the path list (and the query built from it) each iteration mints a reference per
iteration. Python's GC does **not** reclaim these on its own — dropping a plain `int` handle
does not decrement the C refcount — so under the pre-`PathList` bare-`int` contract the
references accumulate for the life of the dictionary. Create both once outside the loop:
> **Source:** `tests/python/test_path_lists.py` snippet `path-list-loop-reuse`
>
> Full runtime pattern: `examples/python/runtime-loop/main.py`
Reuse is the point — the `PathList` finalizer is a safety net that bounds the damage of the
anti-pattern, not a licence to keep writing it.
## Key Types / Functions
All operations are inline wrappers from `path_dictionary_utils.h` taking a
`path_dictionary_instance_t*` (except `ovstage_get_path_dictionary`, which produces one).
| Symbol | Role |
|--------|------|
| `ovstage_get_path_dictionary` | obtain ovstage's `path_dictionary_instance_t*` (owner-owned; do not free) |
| `path_dictionary_create_tokens_from_strings` / `..._get_strings_from_tokens` | string ↔ `ovx_token_t` |
| `path_dictionary_create_paths_from_strings` / `..._create_paths_from_tokens` | build `ovx_primpath_t` (dict-lifetime) |
| `path_dictionary_get_tokens_from_paths` | decompose a `ovx_primpath_t` into its tokens |
| `path_dictionary_create_path_list_from_paths` / `..._create_path_list_from_strings` | build an `ovx_primpath_list_t`, returned with **refcount=1** owned by the caller |
| `path_dictionary_get_paths_from_path_list` / `..._get_num_paths_from_path_list` | read a list back |
| `path_dictionary_add_path_list_reference` / `..._release_path_list_reference` | increment / decrement a list's refcount; release frees the list at zero — pair every `create`/`add` with exactly one release (replaces the legacy `destroy_path_list` slot) |
| `path_dictionary_release_error` | free the `ovx_string_t` error attached to an `ovx_api_result_t` |
Every slot returns `ovx_api_result_t { ovx_api_status_t status; ovx_string_t error; }`, where
`ovx_api_status_t` is `OVX_API_SUCCESS` (0) or `OVX_API_ERROR` (1). Handle sentinels
`OVX_INVALID_TOKEN` / `OVX_INVALID_PRIMPATH` / `OVX_INVALID_PRIMPATH_LIST` are all `0` and are
never returned on success.
## Troubleshooting
- A successful mint never returns `0` — treat `OVX_INVALID_TOKEN` /
`OVX_INVALID_PRIMPATH` / `OVX_INVALID_PRIMPATH_LIST` (all `0`) as "unset/invalid".
- `create_*` slots intern on miss (create-on-miss), so there is no separate lookup call;
empty token/path strings are rejected. Both path-list constructors accept zero paths and mint
a valid empty list; their input array may be null when the count is zero.
`get_*` slots require a live
handle — calling one on a released/unknown path list returns `OVX_API_ERROR`.
- On the dictionary ovstage hands out, `path_dictionary_get_tokens_from_paths` rejects
`OVX_INVALID_PRIMPATH` and unknown or expired prim-path handles with an actionable
`OVX_API_ERROR` and leaves the caller's output buffers untouched; Python
`PathDictionary.path_to_string` propagates that failure as `OvxError` instead of silently
returning an empty string. A valid root path still succeeds, with a zero-token decomposition,
which Python joins to `/` (matching `SdfPath`) — so `path_to_string` never returns an empty
string on success, and every valid handle round-trips back through `intern_path`.
Scope note: this is ovstage's implementation of the shared `ovx` vtable slot. Other
implementations behind the same slot may still report a successfully processed zero-token
path for a bad handle, so do not carry this guarantee into non-ovstage code.
- Pair every `path_dictionary_create_path_list_from_*` / `..._add_path_list_reference` with
exactly one `..._release_path_list_reference`; the list is freed when its refcount reaches
zero. `OVX_INVALID_PRIMPATH_LIST` is a no-op on add/release. Do not release ovstage's
reference on a list that came from a read result (it is a borrow), and do not release an
unknown or already-freed handle — those calls return `OVX_API_ERROR`.
- String pointers from the dictionary are borrowed; copy before the dictionary is
destroyed.
- For passing a name as either a token or a raw string, see `ovx_string_or_token_t` in the
`string-handling` skill (set `token` for hot paths, else `string`).
## References
- Use the `> **Source:**` directives in this skill to locate tested snippets before reusing API patterns.
- `string-handling` skill — `ovx_string_t` / `ovx_string_or_token_t` dual-mode usage.
- `error-handling` skill — ovstage-level error handling (distinct `ovstage_api_status_t` codes).
- Keep related skills, docs, and snippets synchronized when changing the workflow.
View on GitHub