
/// Run conversion using a callback-based visitor.
///
/// Returns a heap-allocated result on success, or null on failure.
/// Check `{{ prefix }}_last_error_code` / `{{ prefix }}_last_error_context` for error details.
/// The returned pointer must be freed with the matching result free function.
///
/// # Safety
///
/// `html` must be a valid, non-null, null-terminated UTF-8 string.
/// `options` must be a valid pointer or null.
/// `visitor` must have been created with `{{ prefix }}_visitor_create`, or be null.
/// Returned pointer must be freed with the matching result free function.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {{ with_visitor_fn_name }}(
{% for param in params %}
    {{ param }},
{% endfor %}
    visitor: *mut {{ pascal_prefix }}Visitor,
) -> *mut {{ return_type }} {
    clear_last_error();

{{ param_conversions }}    struct VisitorRef(*mut {{ pascal_prefix }}Visitor);
    impl std::fmt::Debug for VisitorRef {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            f.debug_struct("VisitorRef").finish_non_exhaustive()
        }
    }
    // SAFETY: VisitorRef is a thin wrapper around a raw pointer to {{ pascal_prefix }}Visitor which
    // is itself Send + Sync. The caller guarantees the pointer remains valid during conversion.
    unsafe impl Send for VisitorRef {}
    // SAFETY: see Send impl above.
    unsafe impl Sync for VisitorRef {}
    impl {{ trait_path }} for VisitorRef {
{{ visitor_ref_methods }}    }
    let visitor_handle: Option<std::sync::Arc<std::sync::Mutex<dyn {{ trait_path }} + Send>>> = if visitor.is_null() {
        None
    } else {
        Some(std::sync::Arc::new(std::sync::Mutex::new(VisitorRef(visitor))))
    };

{{ call }}
        Ok(result) => Box::into_raw(Box::new(result)),
        Err(e) => {
            set_last_error(2, &e.to_string());
            std::ptr::null_mut()
        }
    }
}
