Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make two otherwise identical pointer types incompatible

On certain architectures it may be necessary to have different pointer types for otherwise identical objects. Particularly for a Harvard architecture CPU, you may need something like:

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

Particularly this is how the definition of pointers to ROM / RAM looked like in MPLAB C18 (now discontinued) for PICs. It could define even things like:

char const rom* ram* ram strdptr;

Which means a pointer in RAM to pointers in RAM pointing to strings in ROM (using ram is not necessary as by default things are in RAM by this compiler, just added all for clarity).

The good thing in this syntax is that the compiler is capable to alert you when you try to assign in an incompatible manner, like the address of a ROM location to a pointer to RAM (so something like data1 = data2;, or passing a ROM pointer to a function using a RAM pointer would generate an error).

Contrary to this, in avr-gcc for the AVR-8, there is no such type safety as it rather provides functions to access ROM data. There is no way to distinguish a pointer to RAM from a pointer to ROM.

There are situations where this kind of type safety would be very beneficial to catch programming errors.

Is there some way to add similar modifiers to pointers in some manner (such as by preprocessor, expanding to something which could mimic this behavior) to serve this purpose? Or even something which warns on improper access? (in case of avr-gcc, trying to fetch values without using the ROM access functions)

like image 942
Jubatian Avatar asked Apr 09 '18 12:04

Jubatian


3 Answers

One trick is to wrap the pointers in a struct. Pointers to struct have better type safety than pointers to the primitive data types.

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
like image 144
Lundin Avatar answered Nov 15 '22 16:11

Lundin


Since I received several answers which offer different compromises on providing a solution, I decided to merge them in one, outlining the benefits and drawbacks of each. So you can choose the most appropriate for your particular situation

Named Address Spaces

For the particular problem of solving this, and only this case of ROM and RAM pointers on an AVR-8 micro, the most appropriate solution is this.

This was a proposal for C11 which didn't make it into the final standard, however there are C compilers which support it, including avr-gcc used for 8 bit AVRs.

The related documentation can be accessed here (part of the online GCC manual, also including other architectures using this extension). It is recommendable over other solutions (such as function-like macros in pgmspace.h for the AVR-8) as with this, the compiler can make the appropriate checks, while otherwise accessing the data pointed by remains clear and simple.

In particular, if you have a similar problem of porting something from a compiler which offered some sort of named address spaces, like MPLAB C18, this is likely the fastest and cleanest way to do it.

The ported pointers from above would look like as follows:

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

(If possible, one could simplify the process using appropriate preprocessor definitions)

(Original answer by Olaf)

Struct encapsulation, pointer inside

This method aims to strenghten typing of pointers by wrapping them in structures. The intended usage is that you pass the structures themselves across interfaces, by which the compiler can perform type checks on them.

A "pointer" type to byte data could look like this:

typedef struct{
    uint8_t* ptr;
}bytebuffer_ptr;

The pointed data can be accessed as follows:

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

A function prototype accepting such a type and returning one could look like as follows:

bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len);

(Original answer by dvhh)

Struct encapsulation, pointer outside

Similar to the method above, it aims to strenghten typing of pointers by wrapping them in structures, but in a different manner, providing a more robust constraint. The data type to be pointed to is which is encapsulated.

A "pointer" type to byte data could look like this:

typedef struct{
    uint8_t val;
}byte_data;

The pointed data can be accessed as follows:

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

A function prototype accepting such a type and returning one could look like as follows:

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

(Original answer by Lundin)

Which should I use?

Named Address Spaces in this regard don't need much discussion: They are the most appropriate solution if you only want to deal with a pecularity of your target handling address spaces. The compiler will provide you the compile-time checks you need, and you don't have to try to invent anything further.

If, however for other reasons you are interested in structure wrapping, these are matters which you may want to consider:

  • Both methods can be optimized just fine: at least GCC will generate identical code from either to using plain pointers. So you don't really have to consider performance: they should work.

  • Pointer inside is useful if you have either third-party interfaces to serve which demand pointers, or maybe if you are refactoring something so large which you can't do in one pass.

  • Pointer outside provides more robust type safety as you reinforce the pointed type itself with it: you have a true distinct type which you can't easily (accidentally) convert (implicit cast).

  • Pointer outside allows you to use modifiers on the pointer, such as adding const, which is important for creating robust interfaces (you can make data intended to be read only by a function const).

  • Keep in mind that some people might not like either of these, so if you are working in a group, or are creating code which might be reused by known parties, discuss the matter with them first.

  • Should be obvious, but keep in mind that encapsulating doesn't solve the problem of requiring special access code (such as by the pgmspace.h macros on an AVR-8), assuming no Named Address Spaces are used alongside with the method. It only provides a method to produce a compile error if you try to use a pointer by functions operating on a different address space than what it intends to point into.

Thank you for all the answers!

like image 37
Jubatian Avatar answered Nov 15 '22 16:11

Jubatian


You could encapsulate the pointer in different struct for RAM and ROM, making the type incompatible, but containing the same type of values.

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
}

During compilation :

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

You could of course typedefs the struct for brevity

like image 26
dvhh Avatar answered Nov 15 '22 17:11

dvhh