Unions
A union stores several members in the same memory. Writing one member and reading another reads the same bytes as a different type, which is how C programs look at the bits of a float or describe data that can take one of several layouts. Unions work like C's unions and have the same layout, so they can be shared with C code.
Declaring a union
A union is declared like a record, with union in place of rec. T() creates a union with every byte zero, as does declaring a variable without a value:
Bits: union{ float: f32 raw: u32} main: fun{ var bits = Bits() printf("%x\n", bits.raw) bits.float = 1.0 printf("%x\n", bits.raw) bits.raw = 0x40490fdb printf("%.5f\n", f64(bits.float))}0 3f800000 3.14159
Writing bits.float stores the four bytes of the float, and reading bits.raw gives those same bytes as an integer. Nothing records which member was written last; every member can be read at any time.
Creating a union from a member
T(value) with one argument creates a union with the member whose type is exactly the type of value set to it. Bytes the member doesn't cover are zero:
Bits: union{ float: f32 raw: u32} bitsOf: fun(value: f32) -> u32 => Bits(value).raw main: fun{ printf("%x %x\n", bitsOf(1.0), bitsOf(-2.0)) printf("%.1f\n", f64(Bits(0x40490fdbu32).float))}3f800000 c0000000 3.1
The member is picked by type alone, so the argument's type must be known. An untyped literal like 1 has none and needs a suffix, as in 1u32. A union with two members of the argument's type can't pick one either:
Bits: union{ float: f32 raw: u32} Twin: union{ first: u32 second: u32} main: fun{ val bits = Bits(1)No constructor for 'Bits' accepts these arguments'Bits' takes no arguments, or one value of a field's type: f32, u32 val twin = Twin(1u32)Conversion to 'Twin' is ambiguous}The first error's note lists what Bits accepts: "'Bits' takes no arguments, or one value of a field's type: f32, u32".
Members share one address
Every member of a union starts at offset 0, so they all have the same address:
Bits: union{ float: f32 raw: u32 bytes: array[u8, 4]} main: fun{ var bits = Bits() val a = isize(@bits.float) val b = isize(@bits.raw) val c = isize(@bits.bytes) printf("%d %d\n", i32(a == b), i32(b == c)) bits.raw = 0x11223344 printf("%x %x\n", bits.bytes[0], bits.bytes[3])}1 1 44 11
A member smaller than the union covers only its first bytes. On the 64-bit Windows and Linux targets, the lowest byte of an integer comes first in memory, which is why bytes[0] is 0x44.
Reading and writing members
Members are read and written with ., like fields, and so are the parts of a member that is a record or an array. A union can be a field of a record, and a record can be a member of a union:
MouseInput: rec{ x: i32 y: i32 buttons: u32} KeyInput: rec{ key: u16 scan: u16 flags: u32 time: u64} InputData: union{ mouse: MouseInput keyboard: KeyInput} Input: rec{ kind: u32 data: InputData} main: fun{ var input = Input(1, InputData()) input.data.keyboard.key = 65 input.data.keyboard.time = 99 printf("%d %d\n", input.data.mouse.x, input.data.keyboard.key)}65 65
mouse.x and keyboard.key start at the same offset, so mouse.x reads the key code in its low bytes.
Reading a member whose type allows only some bit patterns makes the value valid first. A bool read from a union is true when its byte is not zero:
Flags: rec{ on: bool off: bool} Packed: union{ flags: Flags raw: u16} main: fun{ var packed = Packed() packed.raw = 0x0200 printf("%d %d\n", i32(packed.flags.on), i32(packed.flags.off))}0 1
A char read from a union is checked: bytes that aren't a valid character panic.
Letter: union{ letter: char raw: u32} main: fun{ var letter = Letter() letter.raw = 0xFFFFFFFF val _ = letter.letter}Panic at main.ce:13:10: invalid char
Sizes
A union is as large as its largest member, rounded up to a multiple of its alignment, and its alignment is the largest alignment of its members:
Mixed: union{ small: u8 wide: i64 triple: array[u16, 3]} Color: union{ floats: array[f32, 4] ints: array[i32, 4]} main: fun{ printf("%d %d\n", i32(sizeOf(Mixed)), i32(alignOf(Mixed))) printf("%d %d\n", i32(sizeOf(Color)), i32(alignOf(Color)))}8 8 16 4
Constructors, methods and properties
A union can declare constructors, methods and properties, as a record does; Records and Methods and properties cover them. A constructor sets the members it wants through self. Declaring a constructor removes T() and T(value), the same as for records. Properties can give names to the bits of an integer member:
ClearColor: union{ floats: array[f32, 4] uints: array[u32, 4] new: op(red: f32, green: f32, blue: f32) { self.floats = [red, green, blue, 1.0] } alpha: fun(self) -> f32 => self.floats[3]} Flags: union{ raw: u32 visible: prop bool { get(self) => (self.raw & 1) != 0 set(mut self, value) => self.raw = (self.raw & ~1) | u32(value) } layer: prop u32 { get(self) => (self.raw >> 2) & 15 set(mut self, value) => self.raw = (self.raw & ~(15 << 2)) | ((value & 15) << 2) }} main: fun{ val color = ClearColor(0.5, 0.25, 0.125) printf("%.1f %x\n", f64(color.alpha()), color.uints[3]) var flags = Flags() flags.visible = true flags.layer = 5 printf("%d %d %d\n", i32(flags.visible), flags.layer, flags.raw)}1.0 3f800000 1 5 21
A union can't declare a destructor or req members and can't be a ref union:
Holder: union{ count: req i32Cannot declare required fields in unions raw: u32 drop: opCannot declare destructors in unions { }} Shared: ref unionCannot declare 'ref' unions{ value: i32} main: fun{}What a union can hold
A union holds only plain data: integers, floats, bool, char, pointers, cstr, ext enum values, and arrays and records made of them. A member can't have a destructor, hold a borrow, or be a type with no all-zero value such as a function value. Plain enums can't be members either, and neither can records with fields that the union's module can't see or write, such as pvt and pvt set fields:
Name: own rec{ id: i32 drop: op { }} Color: enum{ red green} Locked: rec{ count: pvt set i32} Holds: union{ name: NameCannot store values with destructors in unions callback: fun[i32]Cannot store values without defaults in unions color: ColorCannot store 'Color' values in unions locked: LockedCannot store 'Locked' values in unions raw: u64} main: fun{}All members of a union share one visibility. A member can't be less visible than the others, and a member can't restrict who may write it:
Hidden: union{ a: pvt i32Cannot make union members less visible than other members b: i32} Guarded: union{ a: pvt set i32Cannot restrict writes to union members b: i32} Private: union{ a: pvt i32 b: pvt u32} main: fun{}Unions in constants
Reading a member of a union built from a constant is a constant too, so constants can be written as raw bits. The bits are kept exactly, including those of infinities and NaNs:
F32Bits: union{ raw: u32 value: f32} Word: union{ value: u32 bytes: array[u8, 4]} largest: const f32 = F32Bits(0x7F7FFFFFu32).valueinfinity: const f32 = F32Bits(0x7F800000u32).valueoneBits: const u32 = F32Bits(1.0f32).rawlowByte: const u8 = Word(0x11223344u32).bytes[0] main: fun{ printf("%g %g\n", f64(largest), f64(infinity)) printf("%x %x\n", oneBits, lowByte)}3.40282e+38 inf 3f800000 44
A constant can read a member whose type is an integer, a float, bool, char or an array of them. A bool read this way must be 0 or 1, and a char must be a valid character; otherwise the value isn't a constant. A constant can also hold a whole union, as in infinity: const F32Bits = F32Bits(0x7F800000u32).
Generic unions
A union can take type parameters. A type parameter used in a member must have the ext and default constraints, since a union holds only plain data that is valid when zeroed (see What a union can hold). One generic union can then reinterpret the bits of any such type as another:
Bitcast[From: ext + default, To: ext + default]: union{ from: From to: To} nan: const f32 = Bitcast[u32, f32](0x7FC00000u32).to bitsOf: fun(value: f64) -> u64 => Bitcast[f64, u64](value).to main: fun{ printf("%g %llx\n", f64(nan), bitsOf(-2.0)) printf("%x %x\n", Bitcast[u16, u32](0xABCDu16).to, Bitcast[u64, u16](0x1122334455667788u64).to)}nan c000000000000000 abcd 7788
When the two types have different sizes, the union is as large as the larger one: a smaller From leaves the rest of to zero, and a smaller To reads only the first bytes of from. Without the constraints, the member is rejected:
Bare[T]: union{ value: TCannot store borrows in unions} Plain[T: ext]: union{ value: TCannot store values without defaults in unions}Using unions with C
A union has the layout of the C union with the same members in the same order, so a union or a record that contains one can be passed to C functions, by value or through a pointer, and C can fill it in. Here C's memset writes the bytes of a union through its address:
ext{ memset: fun(dest: ptr, value: i32, count: usize) -> ptr} Bits: union{ float: f32 raw: u32 bytes: array[u8, 4]} main: fun{ var bits = Bits() memset(@bits, 0x3f, sizeOf(Bits)) printf("%x %d %.4f\n", bits.raw, bits.bytes[2], f64(bits.float))}3f3f3f3f 63 0.7471
The record-with-a-union shape from Reading and writing members, a tag field followed by a union, is how C APIs describe a value that can take one of several forms. In Cella's own code, an enum with payloads does the same job and checks which case a value is. Calling C covers declaring C functions and the types that can cross into C.
Cella Documentation