如何使两个在其他方面相同的指针类型不兼容

How to make two otherwise identical pointer types incompatible

在某些体系结构上,可能需要为其他方面相同的对象使用不同的指针类型。特别是对于哈佛架构 CPU,您可能需要这样的东西:

uint8_t const ram* data1;
uint8_t const rom* data2;

特别是在 PIC 的 MPLAB C18(现已停产)中,指向 ROM / RAM 的指针的定义是这样的。它甚至可以定义如下内容:

char const rom* ram* ram strdptr;

这意味着 RAM 中的指针指向 RAM 中的指针,指向 ROM 中的字符串(使用 ram 不是必需的,因为默认情况下,此编译器的内容都在 RAM 中,只是为了清楚起见而添加了所有内容)。

这种语法的好处是,当您尝试以不兼容的方式分配时,编译器能够提醒您,例如将 ROM 位置的地址指向指向 RAM 的指针(例如 data1 = data2;,或者将 ROM 指针传递给使用 RAM 指针的函数会产生错误)。

与此相反,在 AVR-8 的 avr-gcc 中,没有这种类型安全,因为它提供了访问 ROM 数据的功能。无法区分指向 RAM 的指针和指向 ROM 的指针。

在某些情况下,这种类型安全对于捕获编程错误非常有益。

有没有办法以某种方式(例如通过预处理器,扩展到可以模仿这种行为的东西)向指针添加类似的修饰符来达到这个目的?或者甚至是警告不当访问的东西? (如果是 avr-gcc,尝试在不使用 ROM 访问函数的情况下获取值)

您可以将指针封装在RAM和ROM的不同结构中,使类型不兼容,但包含相同类型的值。

struct romPtr {
    void *addr;
};

struct ramPtr {
    void *addr;
};

int main(int argc, char **argv) {
    struct romPtr data1 = {NULL};
    struct romPtr data3 = data1;
    struct ramPtr data2 = data1; // <-- gcc would throw a compilation error here
}

编译期间:

$ cc struct_test.c
struct_test.c: In function ‘main’:
struct_test.c:12:24: error: invalid initializer
  struct ramPtr data2 = data1;
                    ^~~~~

您当然可以typedef为简洁起见使用结构

一个技巧是将指针包装在一个结构中。指向结构的指针比指向原始数据类型的指针具有更好的类型安全性。

typedef struct
{
  uint8_t ptr;
} a_t;

typedef struct
{
  uint8_t ptr;
} b_t;

const volatile a_t* a = (const volatile a_t*)0x1234;
const volatile b_t* b = (const volatile b_t*)0x5678;

a = b; // compiler error
b = a; // compiler error

真正的哈佛架构使用不同的指令来访问不同类型的内存,例如代码(AVR 上的闪存)、数据 (RAM)、硬件外围寄存器 (IO) 以及可能的其他内存。范围内地址的 通常重叠,即相同的值访问不同的内部设备,具体取决于指令。

来到C,如果要使用统一的指针,这意味着你不仅要在指针中编码地址(值),还要编码访问类型(以下"address space")价值。这既可以使用指针值中的附加位来完成,也可以 select run-time 处的适当指令 用于每次访问 。这对生成的代码构成了显着的开销。此外,通常至少某些地址 space 的 "natural" 值中没有备用位(例如,指针的所有 16 位已用于该地址)。所以需要额外的位,至少一个字节。这也会增加内存使用量(主要是 RAM)。

在使用此架构的典型 MCU 上,两者通常都是不可接受的,因为它们已经非常有限。幸运的是,对于大多数应用程序来说,绝对没有必要(或者至少可以轻松避免)在 run-time.

处确定地址 space

为了解决这个问题,此类平台的所有编译器都支持某种方式来告诉编译器地址 space 和对象所在的地址。标准草案 N1275 为 then-upcoming C11 提出了使用 "named address spaces" 的标准方式。不幸的是它没有进入最终版本,所以我们只剩下 compiler-extensions.

对于 gcc(请参阅其他编译器的文档),开发人员实施了原始标准提案。由于地址 space 是 target-specific,代码在不同的架构之间不可移植,但对于 bare-metal 嵌入式代码通常是这样,没有什么真正丢失。

阅读 AVR 的 documentation,地址 space 的使用类似于标准限定符。编译器会自动发出正确的指令来访问正确的 space。还有一个统一的地址 space,它决定了 run-time 的区域,如上所述。

地址 space 的工作方式类似于限定符,有更强的约束来确定兼容性,即在将不同地址 space 的指针分配给彼此时。详细说明见proposal, chapter 5.

结论:

命名地址spaces 就是你想要的。他们解决了两个问题:

  • 确保指向不兼容地址 space 的指针不能在不被注意的情况下相互分配。
  • 告诉编译器如何访问对象,即使用哪些指令。

关于提出 structs 的其他答案,您必须在访问数据后指定地址 space(以及 void * 的类型)。在声明中使用地址 space 保持代码的其余部分干净,甚至允许稍后在源代码中的单个位置更改它。

如果您追求 tool-chains 之间的可移植性,请阅读他们的文档并使用宏。您很可能只需要采用地址的实际名称 spaces.

旁注: 您引用的 PIC18 示例实际上使用命名地址 spaces 的语法。只是名称被弃用,因为实现应该为应用程序代码保留所有 non-standard 个名称。因此 gcc 中的 underscore-qualified 名称。

免责声明:我没有测试这些功能,而是依赖文档。感谢评论中的有用反馈。

由于我收到了几个答案,这些答案在提供解决方案时提供了不同的折衷方案,因此我决定将它们合并为一个,概述每个答案的优缺点。所以您可以选择最适合您的具体情况

命名地址空间

对于解决此问题的特定问题,并且只有这种 AVR-8 micro 上的 ROM 和 RAM 指针的情况,最合适的解决方案是这个。

这是针对 C11 的提案,它没有成为最终标准,但是有支持它的 C 编译器,包括用于 8 位 AVR 的 avr-gcc。

可以访问相关文档here(在线 GCC 手册的一部分,还包括使用此扩展的其他体系结构)。它优于其他解决方案(例如 AVR-8 pgmspace.h 中的类似函数的宏),编译器可以进行适当的检查,同时访问指向的数据仍然清晰和简单。

特别是,如果您在从提供某种命名地址 spaces 的编译器(如 MPLAB C18)中移植某些东西时遇到类似问题,这可能是最快和最干净的方法。

从上面移植的指针如下所示:

uint8_t const* data1;
uint8_t const __flash* data2;
char const __flash** strdptr;

(如果可能,可以使用适当的预处理器定义来简化流程)

(Olaf 的原始回答)

结构体封装,指针在里面

此方法旨在通过将指针包装在结构中来加强指针的类型化。预期用途是跨接口传递结构本身,编译器可以通过接口对它们执行类型检查。

"pointer" 类型到字节数据可能如下所示:

typedef struct{
    uint8_t* ptr;
}bytebuffer_ptr;

指向的数据可以通过以下方式访问:

bytebuffer_ptr bbuf;
(...)
bbuf.ptr = allocate_bbuf();
(...)
bbuf.ptr[index] = value;

接受这种类型并返回类型的函数原型可能如下所示:

bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len);

(dvhh 的原始回答)

结构体封装,指针在外

与上述方法类似,它旨在通过将指针包装在结构中来加强指针的类型,但以不同的方式提供更强大的约束。要指向的数据类型是封装的。

"pointer" 类型到字节数据可能如下所示:

typedef struct{
    uint8_t val;
}byte_data;

指向的数据可以通过以下方式访问:

byte_data* bbuf;
(...)
bbuf = allocate_bbuf();
(...)
bbuf[index].val = value;

接受这种类型并返回类型的函数原型可能如下所示:

byte_data* encode_buffer(byte_data* inbuf, size_t len);

(Lundin 的原始回答)

我应该使用哪个?

命名地址空间在这方面不需要太多讨论:如果您只想处理目标处理地址 space 的特殊性,它们是最合适的解决方案。编译器将为您提供所需的编译时检查,您不必再尝试发明任何东西。

如果您出于其他原因对结构包装感兴趣,则可能需要考虑以下事项:

  • 这两种方法都可以很好地优化:至少 GCC 会从使用普通指针的任何一种生成相同的代码。所以你真的不必考虑性能:它们应该可以工作。

  • 如果您有第三方接口来服务于哪些需求指针,或者如果您重构的东西太大而您无法一次性完成,那么内部指针很有用。

  • 外部指针提供了更强大的类型安全性,因为您用它加强了指向类型本身:您拥有一个真正独特的类型,您不能轻易(意外)转换(隐式转换)。

  • Pointer outside 允许您在指针上使用修饰符,例如添加 const,这对于创建健壮的接口很重要(您可以使数据仅供函数读取const).

  • 请记住,有些人可能不喜欢其中任何一种,因此如果您在团队中工作,或者正在创建可能被已知各方重用的代码,请先与他们讨论此事.

  • 应该很明显,但请记住,封装并不能解决需要特殊访问代码的问题(例如 AVR-8 上的 pgmspace.h 宏),假设没有命名地址空间与该方法一起使用。如果您尝试通过在不同地址 space 上运行的函数使用指针,而不是它打算指向的地址,它仅提供一种产生编译错误的方法。

谢谢大家的回答!