{% if flag?(:preview_mt) %} require "crystal/rw_lock" {% end %} require "crystal/tracing" # MUSL: On musl systems, libpthread is empty. The entire library is already included in libc. # The empty library is only available for POSIX compatibility. We don't need to link it. # # Darwin: `libpthread` is provided as part of `libsystem`. There's no reason to link it explicitly. # # Interpreter: Starting with glibc 2.34, `pthread` is integrated into `libc` # and may not even be available as a separate shared library. # There's always a static library for compiled mode, but `Crystal::Loader` does not support # static libraries. So we just skip `pthread` entirely. The symbols are still # available in the interpreter because they are loaded in the compiler. # # OTHERS: On other systems, we add the linker annotation here to make sure libpthread is loaded # before libgc which looks up symbols from libpthread. {% unless flag?(:win32) || flag?(:musl) || flag?(:darwin) || flag?(:android) || (flag?(:interpreted) && flag?(:gnu)) %} @[Link("pthread")] {% end %} {% if flag?(:freebsd) || flag?(:dragonfly) %} {% if flag?(:interpreted) %} # FIXME: We're not using the pkg-config name here because that would resolve the # lib flags for libgc including `-lpthread` which the interpreter is not able # to load on systems with modern libc where libpthread is only available as an # (empty) static library. @[Link("gc-threaded")] {% else %} @[Link("gc-threaded", pkg_config: "bdw-gc-threaded")] {% end %} {% elsif flag?(:interpreted) %} # FIXME: We're not using the pkg-config name here because that would resolve the # lib flags for libgc including `-lpthread` which the interpreter is not able # to load on systems with modern libc where libpthread is only available as an # (empty) static library. @[Link("gc")] {% else %} @[Link("gc", pkg_config: "bdw-gc")] {% end %} # Supported library versions: # # * libgc (8.2.0+; earlier versions require a patch for MT support) # # See https://crystal-lang.org/reference/man/required_libraries.html#other-runtime-libraries {% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %} @[Link(dll: "gc.dll")] {% end %} lib LibGC {% unless flag?(:win32) %} {% pkg_config_name = ((ann = LibGC.annotations(Link).find(&.["pkg_config"])) && ann["pkg_config"]) || ((ann = LibGC.annotations(Link).find(&.[0])) && ann[0]) %} VERSION = {{ `pkg-config #{pkg_config_name} --silence-errors --modversion || printf "0.0.0"`.chomp.stringify }} {% end %} alias Int = LibC::Int alias SizeT = LibC::SizeT {% if flag?(:win32) && flag?(:bits64) %} alias Word = LibC::ULongLong alias SignedWord = LibC::LongLong {% else %} alias Word = LibC::ULong alias SignedWord = LibC::Long {% end %} struct StackBase mem_base : Void* # reg_base : Void* should be used also for IA-64 when/if supported end alias ThreadHandle = Void* struct ProfStats heap_size : Word free_bytes : Word unmapped_bytes : Word bytes_since_gc : Word bytes_before_gc : Word non_gc_bytes : Word gc_no : Word markers_m1 : Word bytes_reclaimed_since_gc : Word reclaimed_bytes_before_gc : Word expl_freed_bytes_since_gc : Word obtained_from_os_bytes : Word end fun init = GC_init fun malloc = GC_malloc(size : SizeT) : Void* fun malloc_atomic = GC_malloc_atomic(size : SizeT) : Void* fun realloc = GC_realloc(ptr : Void*, size : SizeT) : Void* fun free = GC_free(ptr : Void*) fun collect_a_little = GC_collect_a_little : Int fun collect = GC_gcollect fun add_roots = GC_add_roots(low : Void*, high : Void*) fun enable = GC_enable fun disable = GC_disable fun is_disabled = GC_is_disabled : Int fun set_handle_fork = GC_set_handle_fork(value : Int) fun base = GC_base(displaced_pointer : Void*) : Void* fun is_heap_ptr = GC_is_heap_ptr(pointer : Void*) : Int fun general_register_disappearing_link = GC_general_register_disappearing_link(link : Void**, obj : Void*) : Int alias Finalizer = Void*, Void* -> fun register_finalizer = GC_register_finalizer(obj : Void*, fn : Finalizer, cd : Void*, ofn : Finalizer*, ocd : Void**) fun register_finalizer_ignore_self = GC_register_finalizer_ignore_self(obj : Void*, fn : Finalizer, cd : Void*, ofn : Finalizer*, ocd : Void**) fun invoke_finalizers = GC_invoke_finalizers : Int fun get_heap_usage_safe = GC_get_heap_usage_safe(heap_size : Word*, free_bytes : Word*, unmapped_bytes : Word*, bytes_since_gc : Word*, total_bytes : Word*) fun set_max_heap_size = GC_set_max_heap_size(Word) fun get_prof_stats = GC_get_prof_stats(stats : ProfStats*, size : SizeT) fun get_start_callback = GC_get_start_callback : Void* fun set_start_callback = GC_set_start_callback(callback : ->) fun set_push_other_roots = GC_set_push_other_roots(proc : ->) fun get_push_other_roots = GC_get_push_other_roots : -> fun push_all_eager = GC_push_all_eager(bottom : Void*, top : Void*) {% if flag?(:preview_mt) || flag?(:win32) || compare_versions(VERSION, "8.2.0") >= 0 %} fun get_my_stackbottom = GC_get_my_stackbottom(sb : StackBase*) : ThreadHandle fun set_stackbottom = GC_set_stackbottom(th : ThreadHandle, sb : StackBase*) : ThreadHandle {% else %} $stackbottom = GC_stackbottom : Void* {% end %} alias OnHeapResizeProc = Word -> fun set_on_heap_resize = GC_set_on_heap_resize(OnHeapResizeProc) fun get_on_heap_resize = GC_get_on_heap_resize : OnHeapResizeProc enum EventType START # COLLECTION MARK_START MARK_END RECLAIM_START RECLAIM_END END # COLLECTION PRE_STOP_WORLD # STOPWORLD_BEGIN POST_STOP_WORLD # STOPWORLD_END PRE_START_WORLD # STARTWORLD_BEGIN POST_START_WORLD # STARTWORLD_END THREAD_SUSPENDED THREAD_UNSUSPENDED end alias OnCollectionEventProc = EventType -> fun set_on_collection_event = GC_set_on_collection_event(cb : OnCollectionEventProc) fun get_on_collection_event = GC_get_on_collection_event : OnCollectionEventProc alias OnThreadEventProc = EventType, Void* -> fun set_on_thread_event = GC_set_on_thread_event(cb : OnThreadEventProc) fun get_on_thread_event = GC_get_on_thread_event : OnThreadEventProc $gc_no = GC_gc_no : Word $bytes_found = GC_bytes_found : SignedWord # GC_on_collection_event isn't exported. Can't collect totals without it. # bytes_allocd, heap_size, unmapped_bytes are macros fun size = GC_size(addr : Void*) : LibC::SizeT # Boehm GC requires to use its own thread manipulation routines instead of pthread's or Win32's {% if flag?(:win32) %} fun beginthreadex = GC_beginthreadex(security : Void*, stack_size : LibC::UInt, start_address : Void* -> LibC::UInt, arglist : Void*, initflag : LibC::UInt, thrdaddr : LibC::UInt*) : Void* {% elsif !flag?(:wasm32) %} fun pthread_create = GC_pthread_create(thread : LibC::PthreadT*, attr : LibC::PthreadAttrT*, start : Void* -> Void*, arg : Void*) : LibC::Int fun pthread_join = GC_pthread_join(thread : LibC::PthreadT, value : Void**) : LibC::Int fun pthread_detach = GC_pthread_detach(thread : LibC::PthreadT) : LibC::Int {% end %} alias WarnProc = LibC::Char*, Word -> fun set_warn_proc = GC_set_warn_proc(WarnProc) $warn_proc = GC_current_warn_proc : WarnProc fun stop_world_external = GC_stop_world_external fun start_world_external = GC_start_world_external fun get_suspend_signal = GC_get_suspend_signal : Int fun get_thr_restart_signal = GC_get_thr_restart_signal : Int end module GC {% if flag?(:preview_mt) %} @@lock = uninitialized Crystal::RWLock {% end %} # :nodoc: def self.malloc(size : LibC::SizeT) : Void* Crystal.trace :gc, "malloc", size: size do LibGC.malloc(size) end end # :nodoc: def self.malloc_atomic(size : LibC::SizeT) : Void* Crystal.trace :gc, "malloc", size: size, atomic: 1 do LibGC.malloc_atomic(size) end end # :nodoc: def self.realloc(ptr : Void*, size : LibC::SizeT) : Void* Crystal.trace :gc, "realloc", size: size do LibGC.realloc(ptr, size) end end def self.init : Nil {% unless flag?(:win32) %} LibGC.set_handle_fork(1) {% end %} LibGC.init {% if flag?(:preview_mt) %} @@lock = Crystal::RWLock.new {% end %} LibGC.set_start_callback -> do GC.lock_write end # pushes the stack of pending fibers when the GC wants to collect memory: {% unless flag?(:interpreted) %} GC.before_collect do Fiber.unsafe_each do |fiber| fiber.push_gc_roots unless fiber.running? end {% if flag?(:preview_mt) %} Thread.unsafe_each do |thread| if fiber = thread.current_fiber? GC.set_stackbottom(thread.gc_thread_handler, fiber.@stack.bottom) end end {% end %} GC.unlock_write end {% end %} {% if flag?(:tracing) %} if ::Crystal::Tracing.enabled?(:gc) set_on_heap_resize_proc set_on_collection_events_proc end {% end %} # By default the GC warns on big allocations/reallocations. This # is of limited use and pollutes program output with warnings. LibGC.set_warn_proc ->(msg, v) do start = "GC Warning: Repeated allocation of very large block" # This implements `String#starts_with?` without allocating a `String` (#11728) format_string = Slice.new(msg, Math.min(LibC.strlen(msg), start.bytesize)) unless format_string == start.to_slice Crystal::System.print_error msg, v end end end {% if flag?(:tracing) %} @@on_heap_resize : LibGC::OnHeapResizeProc? @@on_collection_event : LibGC::OnCollectionEventProc? @@collect_start = 0_u64 @@mark_start = 0_u64 @@sweep_start = 0_u64 private def self.set_on_heap_resize_proc : Nil @@on_heap_resize = LibGC.get_on_heap_resize LibGC.set_on_heap_resize(->(new_size : LibGC::Word) { Crystal.trace :gc, "heap_resize", size: new_size @@on_heap_resize.try(&.call(new_size)) }) end private def self.set_on_collection_events_proc : Nil @@on_collection_event = LibGC.get_on_collection_event LibGC.set_on_collection_event(->(event_type : LibGC::EventType) { case event_type when .start? @@collect_start = Crystal::System::Time.ticks when .mark_start? @@mark_start = Crystal::System::Time.ticks when .reclaim_start? @@sweep_start = Crystal::System::Time.ticks when .end? duration = Crystal::System::Time.ticks - @@collect_start Crystal.trace :gc, "collect", @@collect_start, duration: duration when .mark_end? duration = Crystal::System::Time.ticks - @@mark_start Crystal.trace :gc, "collect:mark", @@mark_start, duration: duration when .reclaim_end? duration = Crystal::System::Time.ticks - @@sweep_start Crystal.trace :gc, "collect:sweep", @@sweep_start, duration: duration end @@on_collection_event.try(&.call(event_type)) }) end {% end %} def self.collect Crystal.trace :gc, "collect" do LibGC.collect end end def self.enable unless LibGC.is_disabled != 0 raise "GC is not disabled" end LibGC.enable end def self.disable LibGC.disable end def self.free(pointer : Void*) : Nil Crystal.trace :gc, "free" do LibGC.free(pointer) end end def self.add_finalizer(object : Reference) : Nil add_finalizer_impl(object) end def self.add_finalizer(object) # Nothing end private def self.add_finalizer_impl(object : T) forall T LibGC.register_finalizer_ignore_self(object.as(Void*), ->(obj, data) { obj.as(T).finalize }, nil, nil, nil) nil end def self.add_root(object : Reference) roots = @@roots ||= [] of Pointer(Void) roots << Pointer(Void).new(object.object_id) end def self.register_disappearing_link(pointer : Void**) base = LibGC.base(pointer.value) LibGC.general_register_disappearing_link(pointer, base) end def self.is_heap_ptr(pointer : Void*) LibGC.is_heap_ptr(pointer) != 0 end def self.stats LibGC.get_heap_usage_safe(out heap_size, out free_bytes, out unmapped_bytes, out bytes_since_gc, out total_bytes) # collections = LibGC.gc_no - 1 # bytes_found = LibGC.bytes_found Stats.new( # collections: collections, # bytes_found: bytes_found, heap_size: heap_size.to_u64!, free_bytes: free_bytes.to_u64!, unmapped_bytes: unmapped_bytes.to_u64!, bytes_since_gc: bytes_since_gc.to_u64!, total_bytes: total_bytes.to_u64! ) end def self.prof_stats LibGC.get_prof_stats(out stats, sizeof(LibGC::ProfStats)) ProfStats.new( heap_size: stats.heap_size.to_u64!, free_bytes: stats.free_bytes.to_u64!, unmapped_bytes: stats.unmapped_bytes.to_u64!, bytes_since_gc: stats.bytes_since_gc.to_u64!, bytes_before_gc: stats.bytes_before_gc.to_u64!, non_gc_bytes: stats.non_gc_bytes.to_u64!, gc_no: stats.gc_no.to_u64!, markers_m1: stats.markers_m1.to_u64!, bytes_reclaimed_since_gc: stats.bytes_reclaimed_since_gc.to_u64!, reclaimed_bytes_before_gc: stats.reclaimed_bytes_before_gc.to_u64!, expl_freed_bytes_since_gc: stats.expl_freed_bytes_since_gc.to_u64!, obtained_from_os_bytes: stats.obtained_from_os_bytes.to_u64!) end {% if flag?(:win32) %} # :nodoc: def self.beginthreadex(security : Void*, stack_size : LibC::UInt, start_address : Void* -> LibC::UInt, arglist : Void*, initflag : LibC::UInt, thrdaddr : LibC::UInt*) : LibC::HANDLE ret = LibGC.beginthreadex(security, stack_size, start_address, arglist, initflag, thrdaddr) raise RuntimeError.from_errno("GC_beginthreadex") if ret.null? ret.as(LibC::HANDLE) end {% else %} # :nodoc: def self.pthread_create(thread : LibC::PthreadT*, attr : LibC::PthreadAttrT*, start : Void* -> Void*, arg : Void*) LibGC.pthread_create(thread, attr, start, arg) end # :nodoc: def self.pthread_join(thread : LibC::PthreadT) LibGC.pthread_join(thread, nil) end # :nodoc: def self.pthread_detach(thread : LibC::PthreadT) LibGC.pthread_detach(thread) end {% end %} # :nodoc: def self.current_thread_stack_bottom {% if LibGC.has_method?(:get_my_stackbottom) %} th = LibGC.get_my_stackbottom(out sb) {th, sb.mem_base} {% else %} # support for legacy gc releases {Pointer(Void).null, LibGC.stackbottom} {% end %} end # :nodoc: {% if flag?(:preview_mt) %} def self.set_stackbottom(thread_handle : Void*, stack_bottom : Void*) sb = LibGC::StackBase.new sb.mem_base = stack_bottom LibGC.set_stackbottom(thread_handle, pointerof(sb)) end {% else %} def self.set_stackbottom(stack_bottom : Void*) \{% if LibGC.has_method?(:set_stackbottom) %} # this is necessary because Boehm GC does _not_ use `GC_stackbottom` on # Windows when pushing all threads' stacks; it also started crashing on # Linux with libgc after v8.2.4; instead `GC_set_stackbottom` must be used # to associate the new bottom with the running thread sb = LibGC::StackBase.new sb.mem_base = stack_bottom # `nil` represents the current thread (i.e. the only one) LibGC.set_stackbottom(nil, pointerof(sb)) \{% else %} # support for legacy gc releases LibGC.stackbottom = stack_bottom \{% end %} end {% end %} # :nodoc: def self.lock_read {% if flag?(:preview_mt) %} @@lock.read_lock {% end %} end # :nodoc: def self.unlock_read {% if flag?(:preview_mt) %} @@lock.read_unlock {% end %} end # :nodoc: def self.lock_write {% if flag?(:preview_mt) %} @@lock.write_lock {% end %} end # :nodoc: def self.unlock_write {% if flag?(:preview_mt) %} @@lock.write_unlock {% end %} end # :nodoc: def self.push_stack(stack_top, stack_bottom) : Nil LibGC.push_all_eager(stack_top, stack_bottom) end # :nodoc: def self.before_collect(&block) : Nil @@curr_push_other_roots = block @@prev_push_other_roots = LibGC.get_push_other_roots LibGC.set_push_other_roots -> do @@curr_push_other_roots.try(&.call) @@prev_push_other_roots.try(&.call) end end # :nodoc: def self.stop_world : Nil LibGC.stop_world_external end # :nodoc: def self.start_world : Nil LibGC.start_world_external end {% if flag?(:unix) %} # :nodoc: def self.sig_suspend : Signal Signal.new(LibGC.get_suspend_signal) end # :nodoc: def self.sig_resume : Signal Signal.new(LibGC.get_thr_restart_signal) end {% end %} end