refactor(node): switch oaknode to refcounted value handles, migrate consumers

- all 15 OakNode* handle types become neutral by-value structs
  {ctx, addref, release, abi_version}; shared box in
  src/node/c_api/nodehandle.h with owns flag (borrowed accessors
  return non-owning boxes; graph insertion flips owns off)
- oaktimeline/oaktask/oakrender call sites and their own public
  headers migrated to value handles; identity comparisons in
  timeline/task now compare native pointers
- regressions green: oaknode 96, oaktimeline 117, oaktask 106,
  oakrender 44, oakcommon 193, oakcodec 18, oakaudio 36
This commit is contained in:
2026-08-07 17:20:42 +08:00
parent 295ea1bfe5
commit 67176281d9
87 changed files with 4499 additions and 3431 deletions
+74 -54
View File
@@ -21,14 +21,27 @@
#include <gtest/gtest.h>
#include "node/folder.h"
#include "node/node.h"
#include "node/project.h"
#include "../c_api/nodehandle.h"
namespace
{
OakNodeNode *as_node(OakNodeFolder *folder)
OakNodeNode as_node(OakNodeFolder folder)
{
return reinterpret_cast<OakNodeNode *>(folder);
return oaknode_folder_as_node(folder);
}
/**
* @brief Identity compare for value handles (each handle has its own
* control block, so compare the wrapped objects).
*/
template <typename H1, typename H2>
bool same_object(H1 a, H2 b)
{
return oaknode_c_api::to_native<void>(a) == oaknode_c_api::to_native<void>(b);
}
/**
@@ -40,15 +53,19 @@ OakNodeNode *as_node(OakNodeFolder *folder)
* folder edges (upstream bug, reported to the parent agent). Removing
* the edges first keeps teardown clean.
*/
void detach_all(OakNodeFolder *root, OakNodeFolder *a, OakNodeFolder *b)
void detach_all(OakNodeFolder root, OakNodeFolder a, OakNodeFolder b)
{
if (oaknode_folder_index_of_child(root, as_node(a)) >= 0) {
if (!root.ctx) {
return;
}
if (a.ctx && oaknode_folder_index_of_child(root, as_node(a)) >= 0) {
oaknode_folder_remove_child(root, as_node(a));
}
if (oaknode_folder_index_of_child(root, as_node(b)) >= 0) {
if (b.ctx && oaknode_folder_index_of_child(root, as_node(b)) >= 0) {
oaknode_folder_remove_child(root, as_node(b));
}
if (oaknode_folder_index_of_child(a, as_node(b)) >= 0) {
if (a.ctx && b.ctx &&
oaknode_folder_index_of_child(a, as_node(b)) >= 0) {
oaknode_folder_remove_child(a, as_node(b));
}
}
@@ -57,19 +74,19 @@ void detach_all(OakNodeFolder *root, OakNodeFolder *a, OakNodeFolder *b)
TEST(NodeFolder, CreateAndHierarchy)
{
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
OakNodeProject project = oaknode_project_init();
ASSERT_NE(project.ctx, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
OakNodeFolder *root = oaknode_project_root(project);
ASSERT_NE(root, nullptr);
OakNodeFolder root = oaknode_project_root(project);
ASSERT_NE(root.ctx, nullptr);
// Creating a folder requires a project
EXPECT_EQ(oaknode_folder_create(nullptr), nullptr);
EXPECT_EQ(oaknode_folder_create(OakNodeProject{}).ctx, nullptr);
OakNodeFolder *a = oaknode_folder_create(project);
OakNodeFolder *b = oaknode_folder_create(project);
ASSERT_NE(a, nullptr);
ASSERT_NE(b, nullptr);
OakNodeFolder a = oaknode_folder_create(project);
OakNodeFolder b = oaknode_folder_create(project);
ASSERT_NE(a.ctx, nullptr);
ASSERT_NE(b.ctx, nullptr);
EXPECT_EQ(oaknode_folder_child_count(root), 0);
EXPECT_EQ(oaknode_folder_child_count(a), 0);
@@ -77,9 +94,9 @@ TEST(NodeFolder, CreateAndHierarchy)
// add_child builds the hierarchy
EXPECT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_child_count(root), 1);
EXPECT_EQ(oaknode_folder_child_at(root, 0), as_node(a));
EXPECT_TRUE(same_object(oaknode_folder_child_at(root, 0), as_node(a)));
EXPECT_EQ(oaknode_folder_index_of_child(root, as_node(a)), 0);
EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), root);
EXPECT_TRUE(same_object(oaknode_folder_parent_of(as_node(a)), root));
EXPECT_EQ(oaknode_folder_add_child(a, as_node(b)), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_has_child_recursive(root, as_node(b)), 1);
@@ -89,32 +106,32 @@ TEST(NodeFolder, CreateAndHierarchy)
// A node can only be in one folder at a time
EXPECT_EQ(oaknode_folder_add_child(root, as_node(b)), OAKNODE_E_STATE);
// Out-of-range child access yields NULL
EXPECT_EQ(oaknode_folder_child_at(root, -1), nullptr);
EXPECT_EQ(oaknode_folder_child_at(root, 1), nullptr);
// Out-of-range child access yields an empty handle
EXPECT_EQ(oaknode_folder_child_at(root, -1).ctx, nullptr);
EXPECT_EQ(oaknode_folder_child_at(root, 1).ctx, nullptr);
// index_of_child on a non-child is E_NOT_FOUND
EXPECT_EQ(oaknode_folder_index_of_child(root, as_node(b)),
OAKNODE_E_NOT_FOUND);
detach_all(root, a, b);
oaknode_project_free(project);
oaknode_project_free(&project);
}
TEST(NodeFolder, RemoveChild)
{
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
OakNodeProject project = oaknode_project_init();
ASSERT_NE(project.ctx, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
OakNodeFolder *root = oaknode_project_root(project);
OakNodeFolder *a = oaknode_folder_create(project);
ASSERT_NE(root, nullptr);
ASSERT_NE(a, nullptr);
OakNodeFolder root = oaknode_project_root(project);
OakNodeFolder a = oaknode_folder_create(project);
ASSERT_NE(root.ctx, nullptr);
ASSERT_NE(a.ctx, nullptr);
ASSERT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_child_count(root), 0);
EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), nullptr);
EXPECT_EQ(oaknode_folder_parent_of(as_node(a)).ctx, nullptr);
// Removing a non-child is E_NOT_FOUND
EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)),
@@ -124,37 +141,37 @@ TEST(NodeFolder, RemoveChild)
EXPECT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_child_count(root), 1);
detach_all(root, a, nullptr);
oaknode_project_free(project);
detach_all(root, a, OakNodeFolder{});
oaknode_project_free(&project);
}
TEST(NodeFolder, MoveChildren)
{
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
OakNodeProject project = oaknode_project_init();
ASSERT_NE(project.ctx, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
OakNodeFolder *root = oaknode_project_root(project);
OakNodeFolder *a = oaknode_folder_create(project);
OakNodeFolder *b = oaknode_folder_create(project);
OakNodeFolder *c = oaknode_folder_create(project);
ASSERT_NE(root, nullptr);
ASSERT_NE(a, nullptr);
ASSERT_NE(b, nullptr);
ASSERT_NE(c, nullptr);
OakNodeFolder root = oaknode_project_root(project);
OakNodeFolder a = oaknode_folder_create(project);
OakNodeFolder b = oaknode_folder_create(project);
OakNodeFolder c = oaknode_folder_create(project);
ASSERT_NE(root.ctx, nullptr);
ASSERT_NE(a.ctx, nullptr);
ASSERT_NE(b.ctx, nullptr);
ASSERT_NE(c.ctx, nullptr);
ASSERT_EQ(oaknode_folder_add_child(root, as_node(a)), OAKNODE_OK);
ASSERT_EQ(oaknode_folder_add_child(root, as_node(b)), OAKNODE_OK);
ASSERT_EQ(oaknode_folder_add_child(a, as_node(c)), OAKNODE_OK);
// Move b and c into a in one call
OakNodeNode *to_move[] = { as_node(b), as_node(c) };
OakNodeNode to_move[] = { as_node(b), as_node(c) };
EXPECT_EQ(oaknode_folder_move_children(to_move, 2, a), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_child_count(root), 1);
// c was already in a and is skipped; a ends up with {c, b}
EXPECT_EQ(oaknode_folder_child_count(a), 2);
EXPECT_EQ(oaknode_folder_parent_of(as_node(b)), a);
EXPECT_EQ(oaknode_folder_parent_of(as_node(c)), a);
EXPECT_TRUE(same_object(oaknode_folder_parent_of(as_node(b)), a));
EXPECT_TRUE(same_object(oaknode_folder_parent_of(as_node(c)), a));
EXPECT_EQ(oaknode_folder_index_of_child(a, as_node(c)), 0);
EXPECT_EQ(oaknode_folder_index_of_child(a, as_node(b)), 1);
@@ -164,29 +181,32 @@ TEST(NodeFolder, MoveChildren)
// A node with no folder is simply appended
EXPECT_EQ(oaknode_folder_remove_child(root, as_node(a)), OAKNODE_OK);
OakNodeNode *one[] = { as_node(a) };
OakNodeNode one[] = { as_node(a) };
EXPECT_EQ(oaknode_folder_move_children(one, 1, root), OAKNODE_OK);
EXPECT_EQ(oaknode_folder_parent_of(as_node(a)), root);
EXPECT_TRUE(same_object(oaknode_folder_parent_of(as_node(a)), root));
// Detach every remaining edge before teardown (see detach_all).
detach_all(root, a, b);
detach_all(a, b, c);
detach_all(b, a, c);
oaknode_project_free(project);
oaknode_project_free(&project);
}
TEST(NodeFolder, NullHandleErrors)
{
EXPECT_EQ(oaknode_folder_child_count(nullptr), OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_child_at(nullptr, 0), nullptr);
EXPECT_EQ(oaknode_folder_add_child(nullptr, nullptr), OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_remove_child(nullptr, nullptr),
EXPECT_EQ(oaknode_folder_child_count(OakNodeFolder{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_move_children(nullptr, 0, nullptr),
EXPECT_EQ(oaknode_folder_child_at(OakNodeFolder{}, 0).ctx, nullptr);
EXPECT_EQ(oaknode_folder_add_child(OakNodeFolder{}, OakNodeNode{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_has_child_recursive(nullptr, nullptr),
EXPECT_EQ(oaknode_folder_remove_child(OakNodeFolder{}, OakNodeNode{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_index_of_child(nullptr, nullptr),
EXPECT_EQ(oaknode_folder_move_children(nullptr, 0, OakNodeFolder{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_parent_of(nullptr), nullptr);
EXPECT_EQ(oaknode_folder_has_child_recursive(OakNodeFolder{},
OakNodeNode{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_index_of_child(OakNodeFolder{}, OakNodeNode{}),
OAKNODE_E_INVALID);
EXPECT_EQ(oaknode_folder_parent_of(OakNodeNode{}).ctx, nullptr);
}