Skip to content

Seelengrab/FieldFlags.jl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FieldFlags.jl

CI Stable CI Nightly docs-stable docs-dev codecov

This package provides two tiny macros to create bitfield-like objects. For example:

@bitflags struct MyFlags
    flag_a
    flag_b
    flag_c
end

creates a new type MyFlags, which stores the boolean flags flag_a, flag_b and flag_c as its fields in a compressed format. Due to compiler limitations, MyFlags is 1 byte large instead of 3 bits. All structs created by @bitflags are a multiple of 8 bits large.

This may change in the future, but apart from that, the sky is the (literal) limit!

The above object can be accessed like any other struct:

julia> using FieldFlags

julia> @bitflags mutable struct Foo
           a
           b
           c
       end

julia> methods(Foo)
# 1 method for type constructor:
 [1] Foo(a::Bool, b::Bool, c::Bool)
     @ none:0

julia> f = Foo(true, false, true)
Foo(true, false, true)

julia> f.a
true

julia> f.b
false

julia> f.c
true

and gives errors when a field that doesn't exist is accessed:

julia> f.z
ERROR: ArgumentError: Objects of type `Foo` have no field `z`
Stacktrace:
 [1] getproperty(x::Foo, s::Symbol)
   @ Main ~/Documents/projects/FieldFlags/src/FieldFlags.jl:57
 [2] top-level scope
   @ REPL[8]:1

as well as when the given struct either doesn't have any fields, or has duplicates:

julia> @bitflags struct Foo
       end
ERROR: LoadError: ArgumentError: `@bitflags` needs at least one field.
Stacktrace:
 [1] bitflags(expr::Expr)
   @ FieldFlags ~/Documents/projects/FieldFlags.jl/src/FieldFlags.jl:34
 [2] var"@bitflags"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ FieldFlags ~/Documents/projects/FieldFlags.jl/src/FieldFlags.jl:114
in expression starting at REPL[2]:1

julia> @bitflags struct Foo2
           a
           a
       end
ERROR: LoadError: ArgumentError: Fields need to be uniquely identifiable!
Stacktrace:
 [1] bitflags(expr::Expr)
   @ FieldFlags ~/Documents/projects/FieldFlags.jl/src/FieldFlags.jl:36
 [2] var"@bitflags"(__source__::LineNumberNode, __module__::Module, expr::Any)
   @ FieldFlags ~/Documents/projects/FieldFlags.jl/src/FieldFlags.jl:114
in expression starting at REPL[4]:1

Nevertheless, getproperty is implemented with efficiency in mind:

julia> foo(f) = f.a
foo (generic function with 1 method)

julia> @code_llvm foo(f)
;  @ REPL[9]:1 within `foo`
define i8 @julia_foo_199(i8 zeroext %0) #0 {
top:
	# [...]
; ┌ @ /home/sukera/Documents/projects/FieldFlags/src/FieldFlags.jl:93 within `getproperty`
   %4 = and i8 %0, 1
; └
  ret i8 %4
}

so no error paths survive during optimization (if the compiler can constant propagate the property access) and keeps being efficient for even large structs:

julia> @bitflags struct Foo3
           a
           b
           c
           d
           e
           f
           g
           h
           i
           j
           k
           l
           m
           n
           o
           p
           q
           r
           s
           t
           u
           v
           w
           x
       end

julia> f = Foo3(rand(Bool, 24)...)
Foo3(false, true, true, false, false, true, true, true, true, true, true, false, true, false, false, true, false, true, false, false, false, false, true, true)

julia> foo(f) = f.w
foo (generic function with 1 method)

julia> @code_llvm foo(f)
;  @ REPL[12]:1 within `foo`
define i8 @julia_foo_269(i24 zeroext %0) #0 {
top:
	# [...] function setup from julia
  %4 = lshr i24 %0, 22
  %5 = trunc i24 %4 to i8
  %6 = and i8 %5, 1
  ret i8 %6
}

julia> @code_native foo(f)
	.text
	.file	"foo"
	.globl	julia_foo_290                   # -- Begin function julia_foo_290
	.p2align	4, 0x90
	.type	julia_foo_290,@function
julia_foo_290:                          # @julia_foo_290
; ┌ @ REPL[12]:1 within `foo`
	# [...] function setup from julia
	shr	eax, 22
	and	al, 1
	.cfi_def_cfa rsp, 8
	ret
.Lfunc_end0:
	.size	julia_foo_290, .Lfunc_end0-julia_foo_290
	.cfi_endproc
; └
                                        # -- End function
	.section	".note.GNU-stack","",@progbits

Bitfields

Apart from bitflags, which guarantee a Bool on property access, there's also @bitfield, for densely packing integers of various sizes into an object. Structs created from either @bitfield or @bitflags can also be marked mutable, to allow setting of fields:

julia> @bitfield mutable struct Foo
           a:1
           b:2
           c:1
       end

julia> f = Foo(0,3,1)
Foo(false, 0x0000000000000003, true)

julia> f.a
false

julia> f.b
0x0000000000000003

julia> f.c
true

julia> isone(f.c)
true

julia> iszero(f.a)
true

julia> f.a = 1
1

julia> f.a
true

One limitation of allowing fields to be set is that the object is declared as mutable, which may cause allocations (just as with any other mutable, which may need to live on the heap) and results in the object no longer being isbits. This may change in the future.

Unnamed fields

Both @bitflags and @bitfield also support unnamed, explicit padding bits:

julia> @bitfield struct Baz
           a:1
           _:7
           b:2
           _:16
           c:1
       end

julia> sizeof(Baz)
4

julia> names = propertynames(Baz(1,2,3))
(:a, :b, :c)

julia> FieldFlags.propertyoffset.(Baz, names)
(0, 8, 26)