glib-aux: add NMPtrArray helper

Add a simple structure that can own a list of pointers.
The main purpose is to return one or more pointers from a function,
where ownership gets transferred.

This is a more limited form of a GPtrArray. Except, it actually NULL
terminates the list, and it requires only one heap allocation for
the structure and the list.

With using NMPtrArrayStack, it even can track the first 8 pointers (in x64_86)
on the stack and avoiding heap allocation altogether. That is of course
only useful, when the ptr array is not to be returned from the function
(to the caller), but instead passed down to called functions which can
fill it.
This commit is contained in:
Thomas Haller 2022-10-28 13:54:57 +02:00
parent 7bdb606a91
commit 3e8ec66df6
No known key found for this signature in database
GPG key ID: 29C2366E4DFC5728
5 changed files with 326 additions and 0 deletions

View file

@ -462,6 +462,8 @@ src_libnm_glib_aux_libnm_glib_aux_la_SOURCES = \
src/libnm-glib-aux/nm-obj.h \
src/libnm-glib-aux/nm-prioq.c \
src/libnm-glib-aux/nm-prioq.h \
src/libnm-glib-aux/nm-ptr-array.c \
src/libnm-glib-aux/nm-ptr-array.h \
src/libnm-glib-aux/nm-random-utils.c \
src/libnm-glib-aux/nm-random-utils.h \
src/libnm-glib-aux/nm-ref-string.c \

View file

@ -18,6 +18,7 @@ libnm_glib_aux = static_library(
'nm-ref-string.c',
'nm-secret-utils.c',
'nm-shared-utils.c',
'nm-ptr-array.c',
'nm-time-utils.c',
'nm-uuid.c',
),

View file

@ -0,0 +1,153 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
#include "nm-ptr-array.h"
#include "libnm-std-aux/nm-std-utils.h"
/*****************************************************************************/
#define _MALLOCSIZE_FROM_RESERVED(reserved) \
(G_STRUCT_OFFSET(NMPtrArray, ptrs) + (sizeof(gpointer) * ((reserved) + 1u)))
G_STATIC_ASSERT(sizeof(NMPtrArrayStack) <= NM_UTILS_GET_NEXT_REALLOC_SIZE_104);
G_STATIC_ASSERT(G_N_ELEMENTS(((NMPtrArrayStack *) NULL)->_ptrs) > 1);
G_STATIC_ASSERT(G_N_ELEMENTS(((NMPtrArrayStack *) NULL)->_ptrs)
== _NM_PTR_ARRAY_MALLOCSIZE_TO_RESERVED(NM_UTILS_GET_NEXT_REALLOC_SIZE_104) + 1u);
G_STATIC_ASSERT(G_STRUCT_OFFSET(NMPtrArray, ptrs) == G_STRUCT_OFFSET(NMPtrArrayStack, _ptrs));
static gsize
_mallocsize_from_reserved(gsize reserved)
{
nm_assert(reserved < ((G_MAXSIZE - G_STRUCT_OFFSET(NMPtrArray, ptrs)) / sizeof(gpointer)) - 1u);
return _MALLOCSIZE_FROM_RESERVED(reserved);
}
static gsize
_mallocsize_to_reserved(gsize size)
{
gsize reserved;
reserved = _NM_PTR_ARRAY_MALLOCSIZE_TO_RESERVED(size);
nm_assert(size >= _mallocsize_from_reserved(reserved));
return reserved;
}
NMPtrArray *
nm_ptr_array_new(GDestroyNotify destroy_fcn, gsize reserved)
{
NMPtrArray *arr;
if (reserved < 3u)
reserved = 3u;
arr = g_malloc(_mallocsize_from_reserved(reserved));
*((gsize *) &arr->len) = 0;
arr->_reserved = reserved;
arr->_destroy_fcn = destroy_fcn;
*((bool *) &arr->is_stack) = FALSE;
arr->ptrs[0] = NULL;
return arr;
}
void
nm_ptr_array_add_n(NMPtrArray **p_arr, gsize n, gpointer *ptrs)
{
NMPtrArray *arr;
gsize new_reserved;
gsize new_len;
nm_assert(p_arr);
nm_assert(*p_arr);
if (n == 0)
return;
arr = *p_arr;
nm_assert(n < G_MAXSIZE - arr->len);
new_len = arr->len + n;
nm_assert(new_len > arr->len);
/* Note that arr->_reserved does not count the element for the
* last trailing NULL. That is, arr->ptrs[arr->_reserved] is valid.
* In other words, new_len may be as large as arr->reserved before
* we need to reallocate, and `arr->ptrs[new_len] = NULL` is correct. */
if (new_len > arr->_reserved) {
gsize n_bytes;
/* We grow the total buffer size using nm_utils_get_next_realloc_size().
* This quite aggressively increases the buffer size. The idea is that
* NMPtrArray is mostly used for short lived purposes, and it's OK to
* waste some space to reduce re-allocation. */
n_bytes = nm_utils_get_next_realloc_size(TRUE, _mallocsize_from_reserved(new_len));
new_reserved = _mallocsize_to_reserved(n_bytes);
nm_assert(new_len <= new_reserved);
nm_assert(n_bytes >= _mallocsize_from_reserved(new_reserved));
if (arr->is_stack) {
NMPtrArray *arr2 = arr;
arr = g_malloc(_mallocsize_from_reserved(new_reserved));
memcpy(arr, arr2, _mallocsize_from_reserved(arr2->_reserved));
*((bool *) &arr->is_stack) = FALSE;
} else {
arr = g_realloc(arr, _mallocsize_from_reserved(new_reserved));
}
arr->_reserved = new_reserved;
*p_arr = arr;
}
memcpy(&arr->ptrs[arr->len], ptrs, sizeof(gpointer) * n);
arr->ptrs[new_len] = NULL;
*((gsize *) &arr->len) = new_len;
}
void
nm_ptr_array_clear(NMPtrArray *arr)
{
if (!arr)
return;
if (arr->len == 0)
return;
if (!arr->_destroy_fcn) {
(*((gsize *) &arr->len)) = 0;
arr->ptrs[0] = NULL;
return;
}
do {
gsize idx;
gpointer p;
idx = (--(*((gsize *) &arr->len)));
p = g_steal_pointer(&arr->ptrs[idx]);
if (p)
arr->_destroy_fcn(p);
} while (arr->len > 0);
}
void
nm_ptr_array_destroy(NMPtrArray *arr)
{
if (!arr)
return;
nm_ptr_array_clear(arr);
if (!arr->is_stack)
g_free(arr);
}

View file

@ -0,0 +1,105 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef __NM_PTR_ARRAY_H__
#define __NM_PTR_ARRAY_H__
typedef struct _NMPtrArray {
const gsize len;
/* How many elements are allocated/reserved for the ptrs array.
* Note that there is always an extra space reserved for the
* NULL termination afterwards. It means, "len" can grow up
* until (including) _reserved, before reallocation is necessary.
*
* In other words, arr->ptrs[arr->_reserved] is allocated and reserved
* for the trailing NULL (but may be uninitialized if the array is shorter). */
gsize _reserved;
GDestroyNotify _destroy_fcn;
const bool is_stack;
/* This will be the NULL terminated list of pointers. If you
* know what you are doing, you can also steal elements from
* the list. */
gpointer ptrs[];
} NMPtrArray;
#define _NM_PTR_ARRAY_MALLOCSIZE_TO_RESERVED(size) \
((((size) -G_STRUCT_OFFSET(NMPtrArray, ptrs)) / sizeof(gpointer)) - 1u)
#define NM_PTR_ARRAY_STACK_RESERVED \
_NM_PTR_ARRAY_MALLOCSIZE_TO_RESERVED(NM_UTILS_GET_NEXT_REALLOC_SIZE_104)
/* For cases where we don't need to pass a NMPtrArray to the caller, we can
* start with a stack-allocated array. That one starts with 8 pointers
* reserved (or 104 bytes) on the stack (on x64_86). 104 is also the magic
* number that is suitable for reallocation. See NM_UTILS_GET_NEXT_REALLOC_SIZE_104.
*
* Usage:
* NMPtrArrayStack arr_stack = NM_PTR_ARRAY_STACK_INIT(g_free);
* nm_auto_ptrarray *arr = &arr_stack.arr;
* ...
* collect_pointers(args, &arr);
* ...
* do_something_with_pointers(arr);
**/
typedef struct {
NMPtrArray arr;
gpointer _ptrs[NM_PTR_ARRAY_STACK_RESERVED + 1];
} NMPtrArrayStack;
#define NM_PTR_ARRAY_STACK_INIT(destroy_fcn) \
((NMPtrArrayStack){ \
.arr = \
{ \
.len = 0, \
._reserved = NM_PTR_ARRAY_STACK_RESERVED, \
._destroy_fcn = ((GDestroyNotify) (destroy_fcn)), \
.is_stack = TRUE, \
}, \
._ptrs = {NULL}, \
})
NMPtrArray *nm_ptr_array_new(GDestroyNotify destroy_fcn, gsize reserved);
void nm_ptr_array_add_n(NMPtrArray **arr, gsize n, gpointer *ptrs);
static inline void
nm_ptr_array_add(NMPtrArray **p_arr, gpointer ptr)
{
NMPtrArray *arr;
nm_assert(p_arr);
nm_assert(*p_arr);
arr = *p_arr;
if (G_LIKELY(arr->len < arr->_reserved)) {
/* Fast-path. We don't need to reallocate. */
arr->ptrs[arr->len] = ptr;
*((gsize *) &arr->len) += 1;
arr->ptrs[arr->len] = NULL;
return;
}
nm_ptr_array_add_n(p_arr, 1, &ptr);
}
static inline NMPtrArray *
nm_ptr_array_set_free_func(NMPtrArray *arr, GDestroyNotify destroy_fcn)
{
nm_assert(arr);
arr->_destroy_fcn = destroy_fcn;
return arr;
}
void nm_ptr_array_clear(NMPtrArray *arr);
void nm_ptr_array_destroy(NMPtrArray *arr);
NM_AUTO_DEFINE_FCN0(NMPtrArray *, _nm_auto_ptrarray, nm_ptr_array_destroy);
#define nm_auto_ptrarray nm_auto(_nm_auto_ptrarray)
#endif /* __NM_PTR_ARRAY_H__ */

View file

@ -14,6 +14,7 @@
#include "libnm-glib-aux/nm-ref-string.h"
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-prioq.h"
#include "libnm-glib-aux/nm-ptr-array.h"
#include "libnm-glib-aux/nm-test-utils.h"
@ -2630,6 +2631,69 @@ test_uid_to_name(void)
/*****************************************************************************/
static void
test_nm_ptr_array(void)
{
const int N_RUN = 100;
int i_run;
for (i_run = 0; i_run < N_RUN; i_run++) {
NMPtrArrayStack arr_stack = NM_PTR_ARRAY_STACK_INIT(g_free);
char sbuf_num[64];
nm_auto_ptrarray NMPtrArray *arr = NULL;
int n_add;
int i_add;
guint counter;
guint n_ptr;
guint i_ptr;
counter = 0;
if (nmtst_get_rand_bool())
arr = nm_ptr_array_new(g_free, nmtst_get_rand_uint32() % 10);
else {
/* NMPtrArray can also start with using a stack allocated array,
* via NMPtrArrayStack/NM_PTR_ARRAY_STACK_INIT(). */
arr = &arr_stack.arr;
}
n_add = nmtst_get_rand_uint32() % 10u;
for (i_add = 0; i_add < n_add; i_add++) {
gpointer ptrs[NM_PTR_ARRAY_STACK_RESERVED + 10];
n_ptr = nmtst_get_rand_uint32() % G_N_ELEMENTS(ptrs);
for (i_ptr = 0; i_ptr < n_ptr; i_ptr++)
ptrs[i_ptr] = g_strdup(nm_sprintf_buf(sbuf_num, "%u", counter++));
if (n_ptr == 1 && nmtst_get_rand_bool())
nm_ptr_array_add(&arr, ptrs[0]);
else
nm_ptr_array_add_n(&arr, n_ptr, ptrs);
if (i_add == n_add - 1 || nmtst_get_rand_one_case_in(5)) {
g_assert_cmpint(arr->len, ==, counter);
g_assert_cmpint(arr->_reserved, >=, counter);
g_assert(arr->_destroy_fcn == g_free);
for (i_ptr = 0; i_ptr < counter; i_ptr++) {
nm_sprintf_buf(sbuf_num, "%u", i_ptr);
g_assert_cmpstr(arr->ptrs[i_ptr], ==, sbuf_num);
}
g_assert(!arr->ptrs[counter]);
}
}
if (nmtst_get_rand_bool()) {
nm_ptr_array_set_free_func(arr, NULL);
for (i_ptr = 0; i_ptr < arr->len; i_ptr++)
nm_clear_g_free(&arr->ptrs[i_ptr]);
}
if (nmtst_get_rand_bool())
nm_clear_pointer(&arr, nm_ptr_array_destroy);
}
}
/*****************************************************************************/
NMTST_DEFINE();
int
@ -2682,6 +2746,7 @@ main(int argc, char **argv)
g_test_add_func("/general/test_nm_prioq", test_nm_prioq);
g_test_add_func("/general/test_nm_random", test_nm_random);
g_test_add_func("/general/test_uid_to_name", test_uid_to_name);
g_test_add_func("/general/test_nm_ptr_array", test_nm_ptr_array);
return g_test_run();
}