Repository: numpy/numpy
Pull request #28985: MNT: constant string arrays instead of pointers in C
Author: login: DimitriPapadopoulos
Labels: name: 03 - Maintenance
Created: 2025-05-16T11:02:51Z | Merged: 2025-05-23T15:38:03Z

See for example:
* [Declaring C String Constants The Right Way](https://eklitzke.org/declaring-c-string-constants-the-right-way)
* [static const char* const arr[] = {“a”, “b”, “c”, “d”};](https://swetava.wordpress.com/2017/04/28/static-const-char-const-arr-a-b-c-d/)


Diff:
diff --git a/numpy/_core/src/common/npy_cpu_features.c b/numpy/_core/src/common/npy_cpu_features.c
index 8810182812e5..f15f636cdb1e 100644
--- a/numpy/_core/src/common/npy_cpu_features.c
+++ b/numpy/_core/src/common/npy_cpu_features.c
@@ -246,7 +246,7 @@ npy__cpu_validate_baseline(void)
 static int
 npy__cpu_check_env(int disable, const char *env) {
 
-    static const char *names[] = {
+    static const char *const names[] = {
         "enable", "disable",
         "NPY_ENABLE_CPU_FEATURES", "NPY_DISABLE_CPU_FEATURES",
         "During parsing environment variable: 'NPY_ENABLE_CPU_FEATURES':\n",
@@ -277,7 +277,7 @@ npy__cpu_check_env(int disable, const char *env) {
     char *notsupp_cur = &notsupp[0];
 
     //comma and space including (htab, vtab, CR, LF, FF)
-    const char *delim = ", \t\v\r\n\f";
+    const char delim[] = ", \t\v\r\n\f";
     char *feature = strtok(features, delim);
     while (feature) {
         if (npy__cpu_baseline_fid(feature) > 0){
diff --git a/numpy/_core/src/multiarray/compiled_base.c b/numpy/_core/src/multiarray/compiled_base.c
index 26b898fa1479..86b60cf75944 100644
--- a/numpy/_core/src/multiarray/compiled_base.c
+++ b/numpy/_core/src/multiarray/compiled_base.c
@@ -920,11 +920,11 @@ arr_interp_complex(PyObject *NPY_UNUSED(self), PyObject *const *args, Py_ssize_t
     return NULL;
 }
 
-static const char *EMPTY_SEQUENCE_ERR_MSG = "indices must be integral: the provided " \
+static const char EMPTY_SEQUENCE_ERR_MSG[] = "indices must be integral: the provided " \
     "empty sequence was inferred as float. Wrap it with " \
     "'np.array(indices, dtype=np.intp)'";
 
-static const char *NON_INTEGRAL_ERROR_MSG = "only int indices permitted";
+static const char NON_INTEGRAL_ERROR_MSG[] = "only int indices permitted";
 
 /* Convert obj to an ndarray with integer dtype or fail */
 static PyArrayObject *
@@ -1465,7 +1465,7 @@ arr_add_docstring(PyObject *NPY_UNUSED(dummy), PyObject *const *args, Py_ssize_t
     PyObject *obj;
     PyObject *str;
     const char *docstr;
-    static char *msg = "already has a different docstring";
+    static const char msg[] = "already has a different docstring";
 
     /* Don't add docstrings */
 #if PY_VERSION_HEX > 0x030b0000
diff --git a/numpy/_core/src/multiarray/stringdtype/casts.cpp b/numpy/_core/src/multiarray/stringdtype/casts.cpp
index f66727501f97..3632e359c9a9 100644
--- a/numpy/_core/src/multiarray/stringdtype/casts.cpp
+++ b/numpy/_core/src/multiarray/stringdtype/casts.cpp
@@ -605,7 +605,7 @@ load_non_nullable_string(char *in, int has_null, const npy_static_string *defaul
     const npy_packed_static_string *ps = (npy_packed_static_string *)in;
     int isnull = NpyString_load(allocator, ps, string_to_load);
     if (isnull == -1) {
-        const char *msg = "Failed to load string for conversion to a non-nullable type";
+        const char msg[] = "Failed to load string for conversion to a non-nullable type";
         if (has_gil)
         {
             PyErr_SetString(PyExc_MemoryError, msg);
@@ -617,7 +617,7 @@ load_non_nullable_string(char *in, int has_null, const npy_static_string *defaul
     }
     else if (isnull) {
         if (has_null) {
-            const char *msg = "Arrays with missing data cannot be converted to a non-nullable type";
+            const char msg[] = "Arrays with missing data cannot be converted to a non-nullable type";
             if (has_gil)
             {
                 PyErr_SetString(PyExc_ValueError, msg);
@@ -821,8 +821,8 @@ static PyType_Slot s2int_slots[] = {
 
 static const char *
 make_s2type_name(NPY_TYPES typenum) {
-    const char *prefix = "cast_StringDType_to_";
-    size_t plen = strlen(prefix);
+    const char prefix[] = "cast_StringDType_to_";
+    size_t plen = sizeof(prefix)/sizeof(char) - 1;
 
     const char *type_name = typenum_to_cstr(typenum);
     size_t nlen = strlen(type_name);
@@ -833,31 +833,36 @@ make_s2type_name(NPY_TYPES typenum) {
         return NULL;
     }
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
     return buf;
 }
 
 static const char *
 make_type2s_name(NPY_TYPES typenum) {
-    const char *prefix = "cast_";
-    size_t plen = strlen(prefix);
+    const char prefix[] = "cast_";
+    size_t plen = sizeof(prefix)/sizeof(char) - 1;
 
     const char *type_name = typenum_to_cstr(typenum);
     size_t nlen = strlen(type_name);
 
-    const char *suffix = "_to_StringDType";
-    size_t slen = strlen(suffix);
+    const char suffix[] = "_to_StringDType";
+    size_t slen = sizeof(prefix)/sizeof(char) - 1;
 
     char *buf = (char *)PyMem_RawCalloc(sizeof(char), plen + nlen + slen + 1);
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
-    strncat(buf, suffix, slen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
+    p += nlen;
+    memcpy(p, suffix, slen);
     return buf;
 }
 
diff --git a/numpy/_core/src/multiarray/stringdtype/static_string.c b/numpy/_core/src/multiarray/stringdtype/static_string.c
index 02ab7d246a7a..1c29bbb67f7e 100644
--- a/numpy/_core/src/multiarray/stringdtype/static_string.c
+++ b/numpy/_core/src/multiarray/stringdtype/static_string.c
@@ -404,7 +404,7 @@ NpyString_release_allocators(size_t length, npy_string_allocator *allocators[])
     }
 }
 
-static const char * const EMPTY_STRING = "";
+static const char EMPTY_STRING[] = "";
 
 /*NUMPY_API
  * Extract the packed contents of *packed_string* into *unpacked_string*.
diff --git a/numpy/_core/src/umath/string_ufuncs.cpp b/numpy/_core/src/umath/string_ufuncs.cpp
index 5b4b67cda625..ffd757815364 100644
--- a/numpy/_core/src/umath/string_ufuncs.cpp
+++ b/numpy/_core/src/umath/string_ufuncs.cpp
@@ -1521,7 +1521,7 @@ init_string_ufuncs(PyObject *umath)
     dtypes[0] = NPY_OBJECT;
     dtypes[1] = NPY_BOOL;
 
-    const char *unary_buffer_method_names[] = {
+    const char *const unary_buffer_method_names[] = {
         "isalpha", "isalnum", "isdigit", "isspace", "islower",
         "isupper", "istitle", "isdecimal", "isnumeric",
     };
@@ -1635,7 +1635,7 @@ init_string_ufuncs(PyObject *umath)
     dtypes[2] = dtypes[3] = NPY_INT64;
     dtypes[4] = NPY_BOOL;
 
-    const char *startswith_endswith_names[] = {
+    const char *const startswith_endswith_names[] = {
         "startswith", "endswith"
     };
 
@@ -1664,7 +1664,7 @@ init_string_ufuncs(PyObject *umath)
 
     dtypes[0] = dtypes[1] = NPY_OBJECT;
 
-    const char *strip_whitespace_names[] = {
+    const char *const strip_whitespace_names[] = {
         "_lstrip_whitespace", "_rstrip_whitespace", "_strip_whitespace"
     };
 
@@ -1691,7 +1691,7 @@ init_string_ufuncs(PyObject *umath)
 
     dtypes[0] = dtypes[1] = dtypes[2] = NPY_OBJECT;
 
-    const char *strip_chars_names[] = {
+    const char *const strip_chars_names[] = {
         "_lstrip_chars", "_rstrip_chars", "_strip_chars"
     };
 
@@ -1750,7 +1750,7 @@ init_string_ufuncs(PyObject *umath)
 
     dtypes[1] = NPY_INT64;
 
-    const char *center_ljust_rjust_names[] = {
+    const char *const center_ljust_rjust_names[] = {
         "_center", "_ljust", "_rjust"
     };
 
@@ -1827,7 +1827,7 @@ init_string_ufuncs(PyObject *umath)
     dtypes[0] = dtypes[1] = dtypes[3] = dtypes[4] = dtypes[5] = NPY_OBJECT;
     dtypes[2] = NPY_INT64;
 
-    const char *partition_names[] = {"_partition_index", "_rpartition_index"};
+    const char *const partition_names[] = {"_partition_index", "_rpartition_index"};
 
     static STARTPOSITION partition_startpositions[] = {
         STARTPOSITION::FRONT, STARTPOSITION::BACK
diff --git a/numpy/_core/src/umath/stringdtype_ufuncs.cpp b/numpy/_core/src/umath/stringdtype_ufuncs.cpp
index 37ae0a39a349..adcbfd3b7480 100644
--- a/numpy/_core/src/umath/stringdtype_ufuncs.cpp
+++ b/numpy/_core/src/umath/stringdtype_ufuncs.cpp
@@ -2605,7 +2605,7 @@ add_object_and_unicode_promoters(PyObject *umath, const char* ufunc_name,
 NPY_NO_EXPORT int
 init_stringdtype_ufuncs(PyObject *umath)
 {
-    static const char *comparison_ufunc_names[6] = {
+    static const char *const comparison_ufunc_names[6] = {
             "equal", "not_equal",
             "less", "less_equal", "greater_equal", "greater",
     };
@@ -2654,7 +2654,7 @@ init_stringdtype_ufuncs(PyObject *umath)
         return -1;
     }
 
-    const char *unary_loop_names[] = {
+    const char *const unary_loop_names[] = {
         "isalpha", "isdecimal", "isdigit", "isnumeric", "isspace",
         "isalnum", "istitle", "isupper", "islower",
     };
@@ -2874,7 +2874,7 @@ init_stringdtype_ufuncs(PyObject *umath)
         &PyArray_StringDType, &PyArray_StringDType
     };
 
-    const char *strip_whitespace_names[] = {
+    const char *const strip_whitespace_names[] = {
         "_lstrip_whitespace", "_rstrip_whitespace", "_strip_whitespace",
     };
 
@@ -2898,7 +2898,7 @@ init_stringdtype_ufuncs(PyObject *umath)
         &PyArray_StringDType, &PyArray_StringDType, &PyArray_StringDType
     };
 
-    const char *strip_chars_names[] = {
+    const char *const strip_chars_names[] = {
         "_lstrip_chars", "_rstrip_chars", "_strip_chars",
     };
 
@@ -3082,7 +3082,7 @@ init_stringdtype_ufuncs(PyObject *umath)
         &PyArray_StringDType
     };
 
-    const char *partition_names[] = {"_partition", "_rpartition"};
+    const char *const partition_names[] = {"_partition", "_rpartition"};
 
     static STARTPOSITION partition_startpositions[] = {
         STARTPOSITION::FRONT, STARTPOSITION::BACK
diff --git a/numpy/f2py/rules.py b/numpy/f2py/rules.py
index 4122f0a49f17..c10d2afdd097 100644
--- a/numpy/f2py/rules.py
+++ b/numpy/f2py/rules.py
@@ -1154,7 +1154,7 @@
         'frompyobj': [
             '    #setdims#;',
             '    capi_#varname#_intent |= #intent#;',
-            ('    const char * capi_errmess = "#modulename#.#pyname#:'
+            ('    const char capi_errmess[] = "#modulename#.#pyname#:'
              ' failed to create array from the #nth# `#varname#`";'),
             {isintent_hide:
              '    capi_#varname#_as_array = ndarray_from_pyobj('


Reviews:
user: login: ngoldbaum
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T12:10:20Z
user: login: DimitriPapadopoulos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:30:28Z
user: login: ngoldbaum
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-18T14:34:45Z
user: login: ngoldbaum
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-18T14:35:02Z

Review comments:
diff_hunk: @@ -833,31 +833,36 @@ make_s2type_name(NPY_TYPES typenum) {
         return NULL;
     }
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
path: numpy/_core/src/multiarray/stringdtype/casts.cpp
user: login: ngoldbaum
body: why this change?
created_at: 2025-05-16T12:09:25Z
author_association: MEMBER
diff_hunk: @@ -833,31 +833,36 @@ make_s2type_name(NPY_TYPES typenum) {
         return NULL;
     }
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
     return buf;
 }
 
 static const char *
 make_type2s_name(NPY_TYPES typenum) {
-    const char *prefix = "cast_";
-    size_t plen = strlen(prefix);
+    const char prefix[] = "cast_";
+    size_t plen = sizeof(prefix)/sizeof(char) - 1;
 
     const char *type_name = typenum_to_cstr(typenum);
     size_t nlen = strlen(type_name);
 
-    const char *suffix = "_to_StringDType";
-    size_t slen = strlen(suffix);
+    const char suffix[] = "_to_StringDType";
+    size_t slen = sizeof(prefix)/sizeof(char) - 1;
 
     char *buf = (char *)PyMem_RawCalloc(sizeof(char), plen + nlen + slen + 1);
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
-    strncat(buf, suffix, slen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
+    p += nlen;
+    memcpy(p, suffix, slen);
path: numpy/_core/src/multiarray/stringdtype/casts.cpp
user: login: ngoldbaum
body: this too
created_at: 2025-05-16T12:10:11Z
author_association: MEMBER
diff_hunk: @@ -833,31 +833,36 @@ make_s2type_name(NPY_TYPES typenum) {
         return NULL;
     }
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
path: numpy/_core/src/multiarray/stringdtype/casts.cpp
user: login: DimitriPapadopoulos
body: See the comment in the relevant commit:
```
Trying to avoid this compiler warning:

 ../numpy/_core/src/multiarray/stringdtype/casts.cpp:860:12: warning: 'char* strncat(char*, const char*, size_t)' specified bound 15 equals source length [-Wstringop-overflow=]
  860 |     strncat(buf, suffix, slen);
      |     ~~~~~~~^~~~~~~~~~~~~~~~~~~
```
The previous code had already been using `memcpy()` instead of `strncpy()` to avoid a similar warning. The new code extends the logic to `strncat()`. In any case, the code should be consistent, and use only one set of methods:
* Use `strcpy()`/`strcat()` all along, relying on the trailing NUL to find the characters to copy. This would avoid compiler warnings. I know these functions feel less secure, but in this case they are not since we know for sure the destination `buf` is large enough.
* Use `strncpy()`/`strncat()` all along, looking for a trailing NUL while copying, but omitting writing the trailing NUL in the destination`buf`, since it is already initialised by `PyMem_RawCalloc()`. This results in the above compiler warning, because the trailing NUL is omitted.
* Use `memcpy()` all along, relying on the already calculated length of the strings. That should be the most efficient option and avoids compiler warnings.
created_at: 2025-05-16T12:30:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -833,31 +833,36 @@ make_s2type_name(NPY_TYPES typenum) {
         return NULL;
     }
 
-    // memcpy instead of strcpy to avoid stringop-truncation warning, since
-    // we are not including the trailing null character
-    memcpy(buf, prefix, plen);
-    strncat(buf, type_name, nlen);
+    // memcpy instead of strcpy/strncat to avoid stringop-truncation warning,
+    // since we are not including the trailing null character
+    char *p = buf;
+    memcpy(p, prefix, plen);
+    p += plen;
+    memcpy(p, type_name, nlen);
path: numpy/_core/src/multiarray/stringdtype/casts.cpp
user: login: ngoldbaum
body: Sounds good 👍 
created_at: 2025-05-18T14:34:45Z
author_association: MEMBER

PR comments:
user: login: ngoldbaum
created_at: 2025-05-18T14:35:31Z
body: I wonder if we could get clang-tidy to lint stuff like this.
author_association: MEMBER
user: login: DimitriPapadopoulos
created_at: 2025-05-18T14:44:43Z
body: I think Linux's [`checkpatch.pl`](https://docs.kernel.org/dev-tools/checkpatch.html) could have caught some of these issues, I don't have experience with `clang-tidy` but wasn't able to find relevant checks.
author_association: CONTRIBUTOR
user: login: charris
created_at: 2025-05-23T15:38:14Z
body: Thanks @DimitriPapadopoulos .
author_association: MEMBER