【问题标题】:Pass multidimensional array to extern "C" function as void *将多维数组作为 void * 传递给外部“C”函数
【发布时间】:2019-07-11 17:02:43
【问题描述】:

我的库中有一个可以很好地处理多维数组的 C 函数:

void    alx_local_maxima_u8 (ptrdiff_t rows, ptrdiff_t cols,
                    const uint8_t arr_in[static restrict rows][static cols],
                    bool arr_out[static restrict rows][static cols])
        __attribute__((nonnull));

我有一个 unsigned char *,我从 openCV 中定义的 class 收到。该指针表示二维数据,但它不是,我必须将它与指针算术(unsigned char *img_pix = img->data + i*img->step + j;)一起使用,我不是特别喜欢。

我创建了一个与图像大小相同的bool数组(这是一个真正的数组,所以我可以使用数组表示法)来存储函数的结果。

我可以编写一个几乎完全相同的 alx_local_maxima_u8() 副本,它只使用一个指针和指针算法,但如果可以的话,我希望能够重复使用它。

以这种方式编写使用void * 的原型来欺骗C++ 是否安全?:

extern "C"
{
[[gnu::nonnull]]
void    alx_local_maxima_u8 (ptrdiff_t rows, ptrdiff_t cols,
                             const void *arr_in,
                             void *arr_out);
}

理论上void * 可以保存C 将接收的任何指针,C 不会访问任何不属于这些指针的数据,所以我看到的唯一问题是将unsigned char * 别名为@ 987654331@,并传递一个void *,其中应该是uint8_t *[],这可能会导致各种链接器错误。另外,我不知道 C bool 和 C++ bool 是否会在内存中转换成相同的东西(我希望如此)。

也许我应该用 C 编写一个包装器,它接收 void * 并将它们传递给实际的函数,这样我就不需要欺骗 C++。

性能是一个问题,但我使用-flto,所以任何包装器都可能会在链接器中消失。

我在启用 POSIX 的 Linux 中使用 GCC (-std=gnu++17)。

【问题讨论】:

  • 为什么不直接使用可移植的兼容定义?
  • 理论上x[m][n]x[m*n] 在任何一种语言中都是不兼容且不可互换的,即使布局相同。然而,在实践中,编译器不太可能做任何险恶的事情,因为数据来自不受其控制的模块并且它不知道其“真实”类型。因此,它必须假设您没有对类型撒谎。当然,随着编译器变得越来越智能,一切皆有可能。
  • @Mgetz 如果我在 C 中使用简单的指针(就像 openCV 编码它的 2D 图像一样),我将失去在 C 中使用数组表示法的能力,这只是一个不。如果你知道一个不会强制修改函数定义的原型,这在两种语言中都有效,并且不会依赖于 UB,请告诉我,因为我是 C++ 的新手,不要知道我能用它做什么。
  • @n.m.这就是我担心的。理论上编译器不应该知道内容,所以它不能做很多坏事,但是当我使用-flto时,我担心链接器将能够做那些事情。并且链接器可以访问所有这些信息。
  • GNU 链接器在 GCC 中间表示上运行,称为 GIMPLE,据我所知,它与语言无关。语法类似于 C,但这没有任何意义。我认为链接器不会使用严格的 C 或 C++ 别名规则,因为并非所有语言都有这样的规则(但您最好让开发人员确定)。当然,有一天这一切都可能改变。

标签: c++ c arrays pointers strict-aliasing


【解决方案1】:

保证 T[N][M] 将包含 NxM 类型 T 的连续对象阻碍了一些其他有用的优化;在 C 的预标准版本中,该保证的主要用途是它允许代码在某些情况下将存储视为一维数组,但在其他情况下是多维数组。不幸的是,标准未能识别由内部数组衰减形成的指针与通过直接或通过void* 将外部数组转换为内部元素类型形成的指针之间的任何区别,即使它们对前者会妨碍后者的用处。

在任何典型的平台上,在没有全程序优化的情况下,ABI 会将指向多维数组元素的指针等同于指向具有相同总数的单维数组元素的指针元素的数量,使得将后者视为前者是安全的。但是,我不相信 C 或 C++ 标准中有任何内容会禁止实现“优化”如下内容:

// In first compilation unit
void inc_element(void*p, int r, int c, int stride)
{
  int *ip = (int*)p;
  ip[r*stride+c]++;
}
// In second compilation unit
int array[5][5];
void inc_element(void*p, int r, int c, int stride);
int test(int i)
{
  if (array[1][0])
    inc_element(array, i, 0, 5);
  return array[1][0];
}

通过将inc_element 的调用替换为array[0][i*5]++,进而可以优化为array[0][0]++。我不认为该标准的作者打算邀请编译器进行此类“优化”,但我认为他们认为激进的优化器不会将未能禁止此类事情解释为邀请。

【讨论】:

    【解决方案2】:

    将数组指针作为const void * 传递应该不会导致任何问题,但请注意bool 在C 和C++ 中可能有不同的表示。对数组基类型使用更明确的类型(例如 unsigned char)会更安全。

    为指针指定此类型也有助于提高可读性,因为矩阵单元可以使用p[r * cols + c] 直接寻址。

    【讨论】:

    • 在 C 和 C++ 中使用不同的类型表示会很疯狂(至少在 POSIX 中,如果 Windows 这样做我不会感到惊讶)。这将使所有使用指向非固定宽度类型(例如intbool)的指针的extern "C" 函数无效。
    • @CacahueteFrito:不能保证一个 C 编译器为平台生成的代码能够调用或被另一个 C 编译器处理的函数调用,更不用说 C++ 编译器了。许多平台都记录了调用和参数传递约定的建议,但这并不普遍。尤其是在 8 位平台上,调用约定可能因地而异,即使在针对相同 CPU 的实现之间也是如此。
    • @supercat 幸运的是,我静态链接到我的库,因此两者都使用相同的编译器 (GCC) 进行编译。此外,这是针对桌面程序 (amd64),因此不太可能发现不兼容性。不过很高兴知道。如果GCC在参数中添加VLA作为扩展会更好,而不必依赖UB :)
    • @CacahueteFrito:根据已发布的基本原理,该标准的作者将许多东西保留为 UB,目的是许多实现将作为“流行扩展”,在可以服务的情况下可预测地处理它们他们的预期目的。如果一个实现如实指定了它将始终以与 ABI 一致的方式执行某些函数调用的情况,那么该规范将定义此类调用的行为,即使标准没有。
    猜你喜欢
    • 2014-01-15
    • 1970-01-01
    • 2021-11-19
    • 2017-04-18
    • 2013-09-13
    • 1970-01-01
    • 2014-11-19
    • 2014-03-16
    相关资源
    最近更新 更多