-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Reference#allocate
, i.e. @[Primitive(:allocate)]
, does a lot of work behind the scenes before the allocated object's #initialize
gets called:
# pseudo-code representation of `Crystal::CodeGenVisitor#allocate_aggregate`
class Reference
def self.allocate : self
{% if ... %} # type contains any inner pointers
obj = __crystal_malloc64(instance_sizeof(self).to_u64!).as(self)
{% else %}
obj = __crystal_malloc_atomic64(instance_sizeof(self).to_u64!).as(self)
{% end %}
Intrinsics.memset(obj.as(Void*), 0_u8, instance_sizeof(self), false)
{% for ivar in @type.instance_vars %}
{% if ivar.has_default_value? %}
pointerof(obj.@{{ ivar }}).value = {{ ivar.default_value }}
{% end %}
{% end %}
set_crystal_type_id(obj)
obj
end
end
This implementation ties object creation to a global allocator, which is either LibC
or LibGC
depending on whether -Dgc_none
was specified during compilation. But there is actually nothing wrong with using storage allocated by something else:
# heapapi.cr
lib LibC
HEAP_ZERO_MEMORY = 0x00000008
fun GetProcessHeap : HANDLE
fun HeapAlloc(hHeap : HANDLE, dwFlags : DWORD, dwBytes : SizeT) : Void*
fun HeapFree(hHeap : HANDLE, dwFlags : DWORD, lpMem : Void*) : BOOL
end
class Foo
@x : Int32
@y = "abc"
def initialize(@x : Int32)
end
def self.new(x : Int32, &)
obj = LibC.HeapAlloc(LibC.GetProcessHeap, LibC::HEAP_ZERO_MEMORY, instance_sizeof(self)).as(self)
pointerof(obj.@y).value = "abc" # ???
set_crystal_type_id(obj)
begin
obj.initialize(x)
yield obj
ensure
# manual memory management!
obj.finalize if obj.responds_to?(:finalize)
LibC.HeapFree(LibC.GetProcessHeap, 0, obj.as(Void*))
end
end
end
# note the different addresses
Foo.new(1) { |foo| p foo } # => #<Foo:0x1d58eb9bc70 @x=1, @y="abc">
Foo.new(2) { |foo| p foo } # => #<Foo:0x1d58eb9c230 @x=2, @y="abc">
p Foo.new(3) # => #<Foo:0x1d590620f60 @x=3, @y="abc">
p Foo.new(4) # => #<Foo:0x1d590620e40 @x=4, @y="abc">
Or even on the stack, as some people have always dreamed:
class Foo
@x : Int32
@y = "abc"
def initialize(@x : Int32)
end
def self.new(x : Int32, &)
buf = uninitialized UInt8[instance_sizeof(self)] # alignment not guaranteed
obj = buf.to_unsafe.as(self)
buf.fill(0)
pointerof(obj.@y).value = "abc" # ???
set_crystal_type_id(obj)
begin
obj.initialize(x)
yield obj
ensure
obj.finalize if obj.responds_to?(:finalize)
end
end
end
Foo.new(1) { |foo| p foo } # => #<Foo:0x100bffacc @x=1, @y="abc">
Foo.new(2) { |foo| p foo } # => #<Foo:0x100bffaa4 @x=2, @y="abc">
p Foo.new(3) # => #<Foo:0x10bbb840f60 @x=3, @y="abc">
p Foo.new(4) # => #<Foo:0x10bbb840e40 @x=4, @y="abc">
In these cases we have manually expanded the instance variable initializers and the set_crystal_type_id
call, but all of this could have been done by a compiler primitive, because it is really just @[Primitive(:allocate)]
without the allocation. So I would like to have a new primitive class method for this, say @[Primitive(:pre_initialize)]
:
class Foo
def self.new(x : Int32, &)
obj = LibC.HeapAlloc(LibC.GetProcessHeap, 0, instance_sizeof(self)).as(self)
Foo.pre_initialize(obj)
begin
obj.initialize(x)
yield obj
ensure
obj.finalize if obj.responds_to?(:finalize)
LibC.HeapFree(LibC.GetProcessHeap, 0, obj.as(Void*))
end
end
def self.new(x : Int32, &)
buf = uninitialized UInt8[instance_sizeof(self)] # alignment not guaranteed
obj = buf.to_unsafe.as(self)
Foo.pre_initialize(obj)
begin
obj.initialize(x)
yield obj
ensure
obj.finalize if obj.responds_to?(:finalize)
end
end
end
Although the discussion is based on reference types, this would also work for value types except that set_crystal_type_id
isn't called. (Non-abstract value types are leaf types and therefore do not carry the type ID in their layout. Abstract value types are taken care of via upcasting.)
I believe this will make custom allocators easier to write in Crystal if we ever go by that route, especially when targetting embedded devices.