1

I am trying to write the following C code in Metal Shading Language inside of a kernel void function:

float f = 2.1;
uint8_t byteArray[sizeof(f)];
for (int a = 0; a < sizeof(f); a++) {
    byteArray[a] = ((uint8_t*)&f)[a];
}

The code is supposed to get a byte array of the float value. When I try to write the same code in Metal Shader Language, I get the following build-time error:

Pointer type must have explicit address space qualifier

I understand that Metal restricts the use of pointers and requires that arguments to a kernel function are provided with an explicit address space attribute such as device, constant, etc. How would I perform the type-cast in Metal Shading Language?

Todd
  • 500
  • 4
  • 10

3 Answers3

2

This isn't C++, so you have to do it the other way.

You can't use unions or reinterpret_cast in MSL properly.

Instead, there's a type for a vector of 4 uint8_ts, it's uchar4.

To do what you are trying to do, you would write it something like this.

float f = 2.1;
uchar4 ff = as_type<uchar4>(f);

Refer to the MSL spec, section 2.19 Type Conversions and Re-interpreting data.

As for address space qualifiers:

Every pointer in Metal Shading Language has an address qualifier. It can be a device, constant, thread, threadgroup and others. Refer to Chapter 4 in the spec.

These address qualifiers come from the fact that there are different memory spaces. You can read more about them in the document above.

Since f is local variable, it's in thread space, so your uint8_t pointer would have a type of thread uint8_t* and not just uint8_t.

So you could probably do it like this:

float f = 2.1;
thread uint8_t* ff = (thread uint8_t*)&f;

But I think the as_type approach is much more clear.

JustSomeGuy
  • 3,677
  • 1
  • 23
  • 31
1

Don't know about metal specifically, but in ordinary C, you'd want to put f and byteArray inside a union

Here's some sample code:

#include <stdio.h>
#include <stdint.h>

union float_byte {
    float f;
    uint8_t byteArray[sizeof(float)];
};

union float_byte u;

void
dotest(float f)
{

    u.f = f;

    printf("%.6f",u.f);
    for (int idx = 0;  idx < sizeof(u.byteArray);  ++idx)
        printf(" %2.2X",u.byteArray[idx]);
    printf("\n");
}

int
main(void)
{

    dotest(2.1);
    dotest(7.6328);

    return 0;
}

Here's the program output:

2.100000 66 66 06 40
7.632800 E6 3F F4 40

UPDATE:

even today, isn't it still technically UB to read a union member that wasn't the last one written to? Although, it sounds like this is widely supported now with implementation-defined behavior. One of many related questions: stackoverflow.com/questions/2310483/… – yano

No, it's not UB for a number of reasons.

It might be "implementation defined" behavior, but only because of the CPU/processor endianness re. the format of a [32 bit] float in memory, if we wished to interpret the bytes in byteArray

But, AFAICT, that doesn't affect OP's issue since the point was just to get a byte buffer [for binary/serialization of the data?].

If one wanted to interpret the data (e.g. designing a DIY F.P. S/W implementation), then the format and endianness of the float would have to be known. This is [probably] IEEE 784 format, and the processor endianness.

But, using the union to just get a byte pointer, there is no issue. It's not even "implementation defined" behavior.

It's just about the same as:

float my_f = f;
uint8_t *byteArray = (uint8_t *) &my_f;

And, it works because it has to work.

Also, the union [as used here] is a common idiom, dating back to the 1970's so it has to be supported.

Also, it just works [because it has to by design].

If we had:

void
funcA(float f)
{

    u.f = f;
}

void
funcB(void)
{
    for (int idx = 0;  idx < sizeof(u.byteArray);  ++idx)
        printf(" %2.2X",u.byteArray[idx]);
    printf("\n");
}

For fun, assume funcA and funcB are in separate .c files. When funcA is called it changes the memory of u [in a predictable way].

Nothing in between changes the layout of u.

Then, we call funcB. The layout of the bytes in byteArray will be the same/predictable data.

This is similar to and works the same way as writing the float to a file as binary data:

#include <unistd.h>

int fd;

void
writefloat(float f)
{
    float my_f = f;

    write(fd,&my_f,sizeof(float));
}

void
writefloat2(float f)
{

    write(fd,&f,sizeof(float));
}

void
writefloat3(float f)
{

    write(fd,&f,sizeof(f));
}

Perhaps this would be easier to see if we used uint32_t instead of float. We could do an endian test. [Note: this is crude and doesn't account for oddball endianness like pdp11/vax]:

#include <stdio.h>
#include <stdint.h>

union uint_byte {
    uint32_t i;
    uint8_t b[sizeof(uint32_t)];
};

union uint_byte u;

int
main(void)
{

    u.i = 0x01020304;
    if (u.b[0] == 0x04)
        printf("cpu is little-endian\n");
    else
        printf("cpu is big-endian\n");

    return 0;
}
Craig Estey
  • 30,627
  • 4
  • 24
  • 48
  • even today, isn't it still technically UB to read a union member that wasn't the last one written to? Although, it sounds like this is widely supported now with implementation-defined behavior. One of many related questions: https://stackoverflow.com/questions/2310483/purpose-of-unions-in-c-and-c – yano Jun 15 '21 at 18:53
  • @yano I've added an update to my answer to address your concerns – Craig Estey Jun 15 '21 at 20:54
-1

In c++ (20) you have bit_cast. In the section 'Notes' you will see, that it is simply implemented as a call to memcpy.

So, in order to avoid 'undefined behaviour' and to be safe (besides of all the previous, it is also the fastest), just do:

memcpy(byteArray, &f, sizeof f); //size of byteArray must be at least: sizeof f
Erdal Küçük
  • 4,810
  • 1
  • 6
  • 11