|
| 1 | +# Pointer Registry |
| 2 | + |
| 3 | +The bindings need to map libxml2 C pointers back to their Ruby wrapper objects. This is used for two purposes: |
| 4 | + |
| 5 | +1. **Object identity** - returning the same Ruby object when the same C pointer is encountered again (documents and detached root nodes) |
| 6 | +2. **GC reachability** - mark functions look up the owning Ruby document to keep it alive while Ruby references exist into the tree |
| 7 | + |
| 8 | +## Design |
| 9 | + |
| 10 | +The registry is a pointer-keyed `st_table` in `ruby_xml_registry.c` with three operations: |
| 11 | + |
| 12 | +```c |
| 13 | +void rxml_registry_register(void *ptr, VALUE obj); |
| 14 | +void rxml_registry_unregister(void *ptr); |
| 15 | +VALUE rxml_registry_lookup(void *ptr); /* Qnil on miss */ |
| 16 | +``` |
| 17 | +
|
| 18 | +The registry is **not** a GC root. It does not keep objects alive. Objects stay alive through the normal mark chains — mark functions look up the registry instead of holding direct references. |
| 19 | +
|
| 20 | +## What Gets Registered |
| 21 | +
|
| 22 | +Only objects that own their underlying C structure are registered: |
| 23 | +
|
| 24 | +| C pointer | Ruby wrapper | Registered when | |
| 25 | +|-----------|-------------|-----------------| |
| 26 | +| `xmlDocPtr` | `XML::Document` | Document is created or parsed | |
| 27 | +| detached root `xmlNodePtr` | `XML::Node` | Node is created or detached via `remove!` | |
| 28 | +
|
| 29 | +Document-owned child nodes are **not** registered. They are lightweight, non-owning wrappers that get fresh Ruby objects each time they are accessed. |
| 30 | +
|
| 31 | +## How Mark Functions Use It |
| 32 | +
|
| 33 | +When Ruby's GC runs the mark phase, node and attr mark functions look up the owning document through the registry: |
| 34 | +
|
| 35 | +```mermaid |
| 36 | +flowchart TD |
| 37 | + Registry["internal registry"] |
| 38 | + DocWrap["Ruby XML::Document"] |
| 39 | + XDoc["xmlDocPtr"] |
| 40 | + DetachedWrap["Detached Ruby XML::Node"] |
| 41 | + DetachedNode["detached root xmlNodePtr"] |
| 42 | + ChildWrap["Ruby XML::Node"] |
| 43 | + ChildNode["document-owned xmlNodePtr"] |
| 44 | +
|
| 45 | + DocWrap -->|owns| XDoc |
| 46 | + XDoc -->|owns| ChildNode |
| 47 | + DetachedWrap -->|owns| DetachedNode |
| 48 | +
|
| 49 | + ChildWrap -.references.-> ChildNode |
| 50 | + ChildWrap -.mark.-> DocWrap |
| 51 | +
|
| 52 | + XDoc -.references.-> Registry |
| 53 | + DetachedNode -.references.-> Registry |
| 54 | + Registry -.references.-> DocWrap |
| 55 | + Registry -.references.-> DetachedWrap |
| 56 | +
|
| 57 | + classDef ruby fill:#f4a0a0,stroke:#8b1f1b,stroke-width:2px; |
| 58 | + classDef xml fill:#e8f1ff,stroke:#5b84c4,stroke-width:2px; |
| 59 | + classDef registry fill:#f5ebcf,stroke:#b89632,stroke-width:2px; |
| 60 | + class DocWrap,DetachedWrap,ChildWrap ruby; |
| 61 | + class XDoc,DetachedNode,ChildNode xml; |
| 62 | + class Registry registry; |
| 63 | + linkStyle 3,5,6 stroke:#5b84c4,stroke-width:2px,stroke-dasharray: 6 4; |
| 64 | + linkStyle 4,7,8 stroke:#cc342d,stroke-width:2px,stroke-dasharray: 6 4; |
| 65 | +``` |
| 66 | + |
| 67 | +For an attached node, the mark function reads `xnode->doc` (maintained by libxml2), looks up the document in the registry, and marks the Ruby document object. For a detached subtree, it walks to the root via parent pointers, looks up the root in the registry, and marks it. |
| 68 | + |
| 69 | +## Lifecycle |
| 70 | + |
| 71 | +Registered pointers must be unregistered before the underlying C structure is freed: |
| 72 | + |
| 73 | +- `rxml_document_free` unregisters the `xmlDocPtr` before calling `xmlFreeDoc` |
| 74 | +- `rxml_node_free` unregisters the detached root before calling `xmlFreeNode` |
| 75 | +- `rxml_node_unmanage` unregisters when a detached node is attached to a document (libxml takes ownership) |
0 commit comments