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_dictionaryfor 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 onepath_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 inpath_dictionary_types.hand thepath_dictionary_utils.hinline wrappers). - Read the relevant
> **Source:**snippet before writing or explaining API usage. - Know the refcount rule for any
ovx_primpath_list_tbefore releasing a reference to it.
Instructions
- Identify whether the task needs a token, a path, or a path list.
- Obtain a dictionary from its owner: for ovstage,
ovstage_get_path_dictionary(instance)returns apath_dictionary_instance_t*(do not free itsvtable/context). All calls below are the inline wrappers frompath_dictionary_utils.hand take that instance. - Intern strings with
path_dictionary_create_tokens_from_strings(tokens) andpath_dictionary_create_paths_from_strings/..._create_paths_from_tokens(prim paths). Resolve tokens back withpath_dictionary_get_strings_from_tokens; decompose a prim path into its tokens withpath_dictionary_get_tokens_from_paths, then resolve those tokens. - Build path lists with
path_dictionary_create_path_list_from_paths(from interned paths) or..._create_path_list_from_strings; read them back withpath_dictionary_get_paths_from_path_list/..._get_num_paths_from_path_list. - Path lists are refcounted:
path_dictionary_create_path_list_from_*returns refcount=1 owned by you, so pair it with exactly onepath_dictionary_release_path_list_reference(which frees the list when the count reaches zero). To keep a list borrowed from an ovstage read result, callpath_dictionary_add_path_list_referencefirst and release that added reference when done; never release ovstage's own reference. - Check every
ovx_api_result_t::statusagainstOVX_API_SUCCESS; onOVX_API_ERRORtheresult.error(anovx_string_t) describes the failure and must be freed withpath_dictionary_release_error. - 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;inthandles,strnames,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_TOKENis0.ovx_primpath_t— interned prim path.OVX_INVALID_PRIMPATHis0.ovx_primpath_list_t— an immutable set/ordering of prim paths.OVX_INVALID_PRIMPATH_LISTis0.
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_tnever 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 itsvtable/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_tokenspoint into dictionary-owned storage and are valid for the dictionary's lifetime — do not free them; copy if you need them longer. (Error strings insideovx_api_result_tare different — free those withpath_dictionary_release_error.) ovx_primpath_list_thandles are explicitly refcounted.path_dictionary_create_path_list_from_*returns a list with refcount=1 owned by the caller;path_dictionary_add_path_list_referenceincrements andpath_dictionary_release_path_list_referencedecrements, freeing the list at zero. Everycreate/addmust 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_referencefirst and release that added reference when finished. - In Python,
create_path_list*returns anovstage.PathListthat owns its reference and releases it onwith-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.cppsnippetintern-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.cppsnippetpath-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:
/* 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.pysnippetintern-and-resolveFollowed by:
examples/python/minimal/main.pysnippetpath-list-query
Resolve a path list back to strings with paths.get_path_strings(list).
Construct standalone (PathDictionary()) or bind to a stage's shared dictionary with
PathDictionary(stage) for zero-conversion sharing. 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.pysnippetpath-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.pysnippetpath-list-loop-reuseFull 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— treatOVX_INVALID_TOKEN/OVX_INVALID_PRIMPATH/OVX_INVALID_PRIMPATH_LIST(all0) 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 returnsOVX_API_ERROR.- On the dictionary ovstage hands out,
path_dictionary_get_tokens_from_pathsrejectsOVX_INVALID_PRIMPATHand unknown or expired prim-path handles with an actionableOVX_API_ERRORand leaves the caller's output buffers untouched; PythonPathDictionary.path_to_stringpropagates that failure asOvxErrorinstead of silently returning an empty string. A valid root path still succeeds, with a zero-token decomposition, which Python joins to/(matchingSdfPath) — sopath_to_stringnever returns an empty string on success, and every valid handle round-trips back throughintern_path. Scope note: this is ovstage's implementation of the sharedovxvtable 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_referencewith exactly one..._release_path_list_reference; the list is freed when its refcount reaches zero.OVX_INVALID_PRIMPATH_LISTis 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 returnOVX_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_tin thestring-handlingskill (settokenfor hot paths, elsestring).
References
- Use the
> **Source:**directives in this skill to locate tested snippets before reusing API patterns. string-handlingskill —ovx_string_t/ovx_string_or_token_tdual-mode usage.error-handlingskill — ovstage-level error handling (distinctovstage_api_status_tcodes).- Keep related skills, docs, and snippets synchronized when changing the workflow.