C language unions in Crystal

I need to access a data structure in 2 different ways:

  • Either in a classical way, using the data types defined in the structure
  • Either by using the underlying bitmap, for reading and writing bits

What’s the idiomatic way to do that in Crystal ?

You can use Crystal unions, which are type-safe.

Then you can also use C unions: just declare them under a lib: https://crystal-lang.org/reference/syntax_and_semantics/c_bindings/union.html

The real question is: why do you need C-like unions? What are you trying to do?

I need it for the development of a genetic algorithm, where a chromosome is defined by a data structure comprising different elementary types, but is subject to mutations on 1 or more random bits throughout the data structure.

I see. I guess without seeing the actual data type there’s not much more help I can give.

Here is a basic example to illustrate my need.

struct Chromo
  property gen1, gen2

  def initialize(@gen1 : UInt8, @gen2 : UInt16)
  end
  
  def bits
    pointerof(@gen1)
  end
  end
end

ch = Chromo.new(33, 66)

I suppose that the 2 integers are quite contiguous in memory

| gen1 : UInt8  | gen2 : UInt16                 |
|               |                               |  
+-----------------------------------------------+          
|0                   1                   2      |                   
|0|1|2|3|4|5|6|7|8|9|0|1|2|3|4|5|6|7|8|9|0|1|2|3| 
+-----------------------------------------------+
|               |                               |  

What I’d like to be able to do is reading or writing any of the 24 bits of the 2 instance variables, something like

x = ch.bits[0..3] 
ch.bits[7] = 1

This assumes that the pointer can be assigned the BitArray(24) type.
Is this possible and if yes, what syntax for ?

In fact, when I think about it, both types of data are much larger than necessary.
gen1 would be satisfied with 3 bits and gen2 with 5, all of which would fit into a UInt8. The bitfields of C allow this.
Is there some sort of workaround for Crystal ?

Thank you for enlightening me on these questions

1 Like

I think that to make sure things are contiguous in memory you need to put a @[Packed] annotation on top of the struct, otherwise fields will be aligned according to the platform. Check https://crystal-lang.org/reference/syntax_and_semantics/annotations/built_in_annotations.html

But then my advice would be to represent the struct as those bits, and provide accessors to get the data. Something like:

@[Packed]
struct Chromo
  @data : StaticArray(Bool, 24)

  def initialize(gen1 : UInt8, gen2 : UInt16)
    @data = StaticArray(Bool, 24).new(false)
    @data.to_unsafe.as(UInt8*).value = gen1
    (@data.to_unsafe.as(UInt8*) + 1).as(UInt16*).value = gen2
  end

  def gen1
    @data.to_unsafe.as(UInt8*).value
  end

  def gen2
    (@data.to_unsafe.as(UInt8*) + 1).as(UInt16*).value
  end
end

chromo = Chromo.new(1, 2)
p chromo
p chromo.gen1
p chromo.gen2

But then returning pointerof from a method is extremely unsafe, I wouldn’t recommend doing that, though maybe there’s no other way. It really depends on what interface you want to have for your type. From your example it looks like you can manipulate bits arbitrarily, so I don’t know.

1 Like

@Asterite Hi, thanks for your advice.

Your sample code does not suit my needs however, because altering @data does not impact the variables gen1 and gen2, and vice versa (due to the fact that StaticArray is an array of bytes, not bits , I suppose).

I tried to use BitArray, but to no avail! So, I think I’ll just work with strings of pseudo-bits 0 and 1 for now !
Thanks anyway.

So, I think StaticArray of Bool doesn’t work well. Well, it does, it’s just that getting/setting the bit of every position doesn’t seem to show the correct result.

You can use a StaticArray of UInt8 though:

struct Chromo
  @data : StaticArray(UInt8, 3)

  def initialize(gen1 : UInt8, gen2 : UInt16)
    @data = StaticArray(UInt8, 3).new(0)
    @data.to_unsafe.as(UInt8*).value = gen1
    (@data.to_unsafe.as(UInt8*) + 1).as(UInt16*).value = gen2
  end

  def gen1
    @data.to_unsafe.as(UInt8*).value
  end

  def gen2
    (@data.to_unsafe.as(UInt8*) + 1).as(UInt16*).value
  end

  def change!
    @data[0] = 10
    @data[1] = 20
    @data[2] = 30
  end
end

chromo = Chromo.new(1, 20)
puts "before"
p chromo.gen1
p chromo.gen2

chromo.change!

puts "after"
p chromo.gen1
p chromo.gen2

Thanks @Asterite, it works, now.
I note this possibility, even if it doesn’t suit me for the moment, because I lose direct access to read/write bits!