crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.49k stars 1.62k forks source link

Making wrappers of C handle types more atomic #14107

Open HertzDevil opened 11 months ago

HertzDevil commented 11 months ago

A lot of handle types coming from C libs are declared as Void*, but this also has the effect of turning their wrapper classes non-atomic. Take LLVM::PassBuilderOptions as an example:

lib LibLLVM
  type PassBuilderOptionsRef = Void*

  fun create_pass_builder_options = LLVMCreatePassBuilderOptions : PassBuilderOptionsRef
  fun dispose_pass_builder_options = LLVMDisposePassBuilderOptions(options : PassBuilderOptionsRef)
end

# allocation calls `GC.malloc` rather than `.malloc_atomic`, because
# `@options` is an internal pointer
class LLVM::PassBuilderOptions
  def initialize
    @options = LibLLVM.create_pass_builder_options
    @disposed = false
  end

  def to_unsafe
    @options
  end

  def finalize
    return if @disposed
    @disposed = true

    LibLLVM.dispose_pass_builder_options(self)
  end
end

Given an unreachable LLVM::PassBuilderOptions object on the heap, Boehm GC will scan the object's contents, but not the contents referred by its @options variable, because it knows that the pointer doesn't belong to its own heap (we cannot pass the GC's allocator functions to LLVM). In some other C libraries, the Void* might not even physically refer to a (virtual) memory address, e.g. most LibC::HANDLEs. If we could guarantee this, we may as well use a regular integer type rather than Void*:

lib LibLLVM
  type PassBuilderOptionsRef = IntPtr
end

Since Crystal no longer sees any pointers inside LLVM::PassBuilderOptions's instance variables, LLVM::PassBuilderOptions will now use GC.malloc_atomic instead, and the GC won't scan the object contents at all.

A similar argument holds for struct wrappers:

lib LibLLVM
  type ValueRef = Void*
end

module LLVM::ValueMethods
  def initialize(@unwrap : LibLLVM::ValueRef)
  end
end

struct LLVM::Value
  include ValueMethods
end

An Array(LLVM::Value) maintains its buffer via Pointer(LLVM::Value).malloc. This uses non-atomic allocation because @unwrap is a pointer, but can be made atomic if LibLLVM::ValueRefValueRef becomes an IntPtr instead. Note that in this case LLVM manages the lifetimes of all LibLLVM::ValueRefs; there is no C API to dispose a value.

IntPtr may be obtained from LibC, or we could also expose it publicly.

oprypin commented 11 months ago

FWIW this will need to be nuanced for sure because it's possible to tell the C library (if it's flexible in that way) to allocate with Crystal's allocator and then actually rely on GC for the C type.