hypercast-wasm 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ module HyperCast
2
+ # Maps the running RUBY_PLATFORM to the RID-style directory (matching the other bindings'
3
+ # runtimes/{rid}/native/ / native/{rid}/ convention) and native library filename to load.
4
+ module NativePlatform
5
+ class UnsupportedPlatformError < StandardError; end
6
+
7
+ # +platform+ is a parameter only so the specs can walk the whole table from one host;
8
+ # every real caller takes the default.
9
+ def self.rid_and_library_name(platform = RUBY_PLATFORM)
10
+ arch = platform.match?(/arm64|aarch64/) ? "arm64" : "x64"
11
+
12
+ case platform
13
+ when /mingw|mswin|windows/
14
+ ["win-#{arch}", "hypercast.dll"]
15
+ when /darwin/
16
+ ["osx-#{arch}", "libhypercast.dylib"]
17
+ when /linux/
18
+ # Two C libraries, two builds: a glibc-linked library cannot be relied on to dlopen
19
+ # into a musl process (Alpine), so musl gets RIDs of its own. Ruby names the libc
20
+ # in its platform string there ("x86_64-linux-musl") and leaves it off on glibc.
21
+ os = platform.include?("musl") ? "linux-musl" : "linux"
22
+ ["#{os}-#{arch}", "libhypercast.so"]
23
+ else
24
+ raise UnsupportedPlatformError, "hypercast: unsupported platform RUBY_PLATFORM=#{platform}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,128 @@
1
+ # Autoloaded, not required: the platform gems carry no library for Fiddle to open and do not
2
+ # declare fiddle, so loading it eagerly would make `require "hypercast"` fail under Bundler
3
+ # wherever fiddle is a bundled rather than a default gem (Ruby 4.0). It loads the first time
4
+ # the Fiddle backend runs, and every path there goes through `functions` first, so a platform
5
+ # gem forced onto Fiddle still gets missing_library_message rather than a bare LoadError.
6
+ autoload :Fiddle, "fiddle"
7
+
8
+ module HyperCast
9
+ # Fiddle plumbing for the native libhypercast shared library — dlopen/dlsym plus raw
10
+ # C-ABI calls, no runtime bridge. Fiddle ships with every Ruby install; it's a plain gem
11
+ # dependency of the universal gem (see hypercast.gemspec) rather than a third-party one.
12
+ # A Ruby gem's files are already plain files on disk once installed, so native/{rid}/{lib}
13
+ # dlopen's directly — no extraction.
14
+ module Runtime
15
+ NATIVE_DIR = File.join(__dir__, "native")
16
+
17
+ # Each door's C signature, by shape — resolved to Fiddle types only in load_functions,
18
+ # so nothing here touches Fiddle until the Fiddle backend actually runs: plain is
19
+ # (text, len, out, fault), numeric adds the packed NumFormat pointer before out, and
20
+ # declared a caller-declared u32 (precision, epoch, field order) in the same place.
21
+ DOORS = {
22
+ cast_bool: :plain,
23
+ cast_i8: :numeric, cast_i16: :numeric, cast_i32: :numeric, cast_i64: :numeric,
24
+ cast_u8: :numeric, cast_u16: :numeric, cast_u32: :numeric, cast_u64: :numeric,
25
+ cast_f32: :numeric, cast_f64: :numeric, cast_decimal: :numeric,
26
+ cast_uuid: :plain,
27
+ cast_timestamp: :plain, cast_unix: :declared, cast_excel_serial: :declared,
28
+ cast_date: :plain, cast_date_ordered: :declared, cast_datetime: :declared,
29
+ cast_time: :plain, cast_duration: :plain
30
+ }.freeze
31
+
32
+ # The one export that is not a door: the zero-argument version probe, returning the
33
+ # core's packed version word (major << 16 | minor << 8 | patch) rather than a verdict
34
+ # code — so it gets its own Fiddle signature beside DOORS instead of a row in it.
35
+ VERSION_PROBE = :hypercast_version
36
+
37
+ @mutex = Mutex.new
38
+ @functions = nil
39
+
40
+ class << self
41
+ # The door's (or VERSION_PROBE's) Fiddle::Function, for the caller to invoke directly
42
+ # — a splat-through `call(symbol, *args)` here built and re-splatted an Array on
43
+ # every cast.
44
+ def function(symbol)
45
+ functions.fetch(symbol)
46
+ end
47
+
48
+ # Every Fiddle allocation goes through here, and loads the library first: that is what
49
+ # keeps a missing library reported as missing_library_message instead of as whatever
50
+ # touching Fiddle raises first. The per-thread scratch buffer and each packed NumFormat
51
+ # (hypercast.rb) pay for it once apiece.
52
+ def buffer(size)
53
+ functions
54
+ Fiddle::Pointer.malloc(size, Fiddle::RUBY_FREE)
55
+ end
56
+
57
+ private
58
+
59
+ # The shared library to dlopen: this install's native/{rid}/{lib}, or — the
60
+ # development loop — the in-repo cargo build, exactly what the other bindings' local
61
+ # staging does. Nil when neither exists.
62
+ def library_path
63
+ rid, lib_name = NativePlatform.rid_and_library_name
64
+ path = File.join(NATIVE_DIR, rid, lib_name)
65
+ return path if File.exist?(path)
66
+
67
+ repo_build = File.expand_path(File.join(__dir__, "../../../rust/target/release", lib_name))
68
+ File.exist?(repo_build) ? repo_build : nil
69
+ end
70
+
71
+ # Why Fiddle found nothing to load. A precompiled platform gem is the one install where
72
+ # that is by design rather than a gap: it carries only its Magnus extensions, and the
73
+ # Fiddle backend is reached there only by forcing it (HYPERCAST_PURE) or because none
74
+ # of its extensions loaded — a gem RubyGems matched to a Ruby it was not built for,
75
+ # such as the glibc Linux gem that `gem install` on RubyGems 3.x picks on Alpine. So
76
+ # that case names its fix, the universal gem, which carries every platform's library,
77
+ # instead of a missing path that reads like a packaging bug. Both arguments are
78
+ # parameters only so the specs can ask for every wording.
79
+ def missing_library_message(gem_platform = Gem.loaded_specs["hypercast"]&.platform,
80
+ forced = ENV.key?("HYPERCAST_PURE"))
81
+ rid, lib_name = NativePlatform.rid_and_library_name
82
+ missing = File.join(NATIVE_DIR, rid, lib_name)
83
+ if gem_platform && gem_platform.to_s != Gem::Platform::RUBY
84
+ reason =
85
+ if forced
86
+ "HYPERCAST_PURE forces the Fiddle backend (unset it to use the extension)"
87
+ else
88
+ "none of its extensions loads on this Ruby (#{RUBY_VERSION}, #{RUBY_PLATFORM})"
89
+ end
90
+ "hypercast: this #{gem_platform} platform gem carries only Magnus extensions, no " \
91
+ "Fiddle library, and #{reason}. The universal gem has the Fiddle backend for every " \
92
+ "platform: `gem install hypercast --platform ruby`, or Bundler's force_ruby_platform " \
93
+ "(#{missing} not found)"
94
+ else
95
+ "hypercast: #{missing} not found (unsupported platform, or this gem was built " \
96
+ "without a native library for it)"
97
+ end
98
+ end
99
+
100
+ # Loaded lazily and exactly once; the native library and its function pointers live
101
+ # for the process's lifetime, same as every other binding (never dlclose'd). The
102
+ # unsynchronized read is the fast path — a per-call mutex acquisition measured as a
103
+ # real slice of the door cost; the benign race re-checks under the lock.
104
+ def functions
105
+ @functions || @mutex.synchronize { @functions ||= load_functions }
106
+ end
107
+
108
+ def load_functions
109
+ path = library_path
110
+ raise LoadError, missing_library_message if path.nil?
111
+
112
+ handle = Fiddle.dlopen(path)
113
+ plain = [Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]
114
+ signatures = {
115
+ plain: plain,
116
+ numeric: plain.dup.insert(2, Fiddle::TYPE_VOIDP),
117
+ declared: plain.dup.insert(2, Fiddle::TYPE_UINT32_T)
118
+ }
119
+ functions = DOORS.to_h do |name, shape|
120
+ [name, Fiddle::Function.new(handle[name.to_s], signatures.fetch(shape), Fiddle::TYPE_INT32_T)]
121
+ end
122
+ functions[VERSION_PROBE] =
123
+ Fiddle::Function.new(handle[VERSION_PROBE.to_s], [], Fiddle::TYPE_UINT32_T)
124
+ functions
125
+ end
126
+ end
127
+ end
128
+ end