Files
mruby-mruby/lib/mruby/amalgam.rb
Yukihiro "Matz" Matsumoto f78ac530c8 amalgam.rb: support gems with core-affecting defines
Gems like mruby-task add preprocessor defines (MRB_USE_TASK_SCHEDULER)
that affect mrb_state structure. The amalgamation generator now detects
these defines from the build configuration and adds them at the top of
mruby.h before struct definitions are encountered.

Supported define patterns: MRB_USE_*, MRB_UTF8_*, HAVE_MRUBY_*

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-02 17:35:31 +09:00

569 lines
18 KiB
Ruby

module MRuby
class Amalgam
# Top-level headers to include (internal dependencies are inlined recursively)
# mruby.h is the main header - it includes value.h, gc.h, version.h with proper macro order
# Note: internal.h is NOT included here - it goes in mruby.c
HEADER_ORDER = %w[
mruby.h
mruby/array.h
mruby/string.h
mruby/hash.h
mruby/class.h
mruby/proc.h
mruby/range.h
mruby/variable.h
mruby/numeric.h
mruby/error.h
mruby/data.h
mruby/istruct.h
mruby/mempool.h
mruby/debug.h
mruby/dump.h
mruby/irep.h
mruby/opcode.h
mruby/re.h
mruby/throw.h
mruby/khash.h
mruby/endian.h
mruby/presym.h
mruby/compile.h
].freeze
# Boxing headers are conditionally included
BOXING_HEADERS = %w[
mruby/boxing_no.h
mruby/boxing_word.h
mruby/boxing_nan.h
].freeze
# Core sources in recommended order
CORE_SOURCE_ORDER = %w[
allocf.c
readnum.c
readint.c
readfloat.c
state.c
symbol.c
class.c
object.c
gc.c
mempool.c
variable.c
array.c
hash.c
string.c
range.c
numeric.c
numops.c
proc.c
kernel.c
enum.c
error.c
backtrace.c
vm.c
load.c
dump.c
cdump.c
codedump.c
print.c
fmt_fp.c
debug.c
etc.c
version.c
init.c
].freeze
def initialize(build)
@build = build
@processed_guards = {}
@processed_headers = [] # Track header paths for include transformation
# Pre-collect gem header names for source include transformation
@gem_header_names = collect_gem_header_names
end
def collect_gem_header_names
names = []
library_gems.each do |gem|
gem_include = "#{gem.dir}/include"
next unless File.directory?(gem_include)
Dir.glob("#{gem_include}/**/*.h").each do |path|
rel_path = path.sub("#{gem_include}/", "")
names << rel_path
# Also track basename for simple includes like "io_hal.h"
names << File.basename(rel_path)
end
end
names.uniq
end
def generate_header(output_path)
FileUtils.mkdir_p(File.dirname(output_path))
_pp "GEN", output_path.relative_path
File.open(output_path, "w:binary") do |f|
write_header_preamble(f)
write_ordered_headers(f)
# Boxing headers are inlined at their include point in value.h
# Presym headers are inlined via presym.h -> enable.h -> id.h
write_gem_headers(f)
write_header_postamble(f)
end
end
def generate_source(output_path)
FileUtils.mkdir_p(File.dirname(output_path))
_pp "GEN", output_path.relative_path
File.open(output_path, "w:binary") do |f|
write_source_preamble(f)
write_internal_headers(f)
write_core_sources(f)
write_generated_sources(f)
write_gem_sources(f)
end
end
private
def include_dir
"#{MRUBY_ROOT}/include"
end
def build_include_dir
"#{@build.build_dir}/include"
end
def src_dir
"#{MRUBY_ROOT}/src"
end
# Filter out binary gems (they have main() functions)
def library_gems
@build.gems.reject { |gem| gem.name.start_with?("mruby-bin-") }
end
# ========== Header Generation ==========
def write_header_preamble(f)
f.puts <<~PREAMBLE
/*
** mruby amalgamated header
** Generated from mruby source files
**
** This file is auto-generated. Do not edit directly.
*/
#ifndef MRUBY_AMALGAM_H
#define MRUBY_AMALGAM_H
#ifdef __cplusplus
#define __STDC_LIMIT_MACROS
#define __STDC_CONSTANT_MACROS
#define __STDC_FORMAT_MACROS
#endif
#include <stdarg.h>
#include <stdint.h>
#include <stddef.h>
#include <limits.h>
#include <string.h>
PREAMBLE
# Add build-level defines from gems (e.g., MRB_USE_TASK_SCHEDULER)
gem_defines = collect_gem_defines
unless gem_defines.empty?
f.puts "/* Gem-required defines */"
gem_defines.each do |d|
f.puts "#define #{d}"
end
f.puts
end
end
# Collect defines added by gems that affect core headers
def collect_gem_defines
defines = []
@build.defines.each do |d|
# Include defines that affect mrb_state or core functionality
defines << d if d =~ /^MRB_USE_|^MRB_UTF8_|^HAVE_MRUBY_/
end
defines.uniq.sort
end
def write_header_postamble(f)
f.puts <<~POSTAMBLE
#endif /* MRUBY_AMALGAM_H */
POSTAMBLE
end
def write_ordered_headers(f)
# Process top-level headers; internal includes are recursively inlined
HEADER_ORDER.each do |header|
path = "#{include_dir}/#{header}"
next unless File.exist?(path)
write_header_content(f, header, path)
end
end
def write_boxing_headers(f)
f.puts "\n/* Boxing type selection */"
f.puts "#if defined(MRB_NAN_BOXING)"
write_header_content(f, "mruby/boxing_nan.h", "#{include_dir}/mruby/boxing_nan.h")
f.puts "#elif defined(MRB_WORD_BOXING)"
write_header_content(f, "mruby/boxing_word.h", "#{include_dir}/mruby/boxing_word.h")
f.puts "#else"
write_header_content(f, "mruby/boxing_no.h", "#{include_dir}/mruby/boxing_no.h")
f.puts "#endif"
end
def write_presym_headers(f)
presym_dir = "#{@build.build_dir}/include/mruby/presym"
return unless File.directory?(presym_dir)
%w[id.h table.h].each do |header|
path = "#{presym_dir}/#{header}"
next unless File.exist?(path)
write_header_content(f, "mruby/presym/#{header}", path)
end
end
def write_gem_headers(f)
library_gems.each do |gem|
gem_include = "#{gem.dir}/include"
next unless File.directory?(gem_include)
Dir.glob("#{gem_include}/**/*.h").sort.each do |path|
rel_path = path.sub("#{gem_include}/", "")
write_header_content(f, "#{gem.name}: #{rel_path}", path)
# Also track the relative path for source include transformation
# (handles includes like #include "io_hal.h")
@processed_headers << rel_path unless @processed_headers.include?(rel_path)
end
end
end
def write_header_content(f, name, path)
return unless File.exist?(path)
content = File.read(path, mode: "rb")
guard = extract_include_guard(content)
# Skip if already processed
if guard && @processed_guards[guard]
f.puts "/* #{name} - already included */"
return
end
@processed_guards[guard] = true if guard
@processed_headers << name # Track header path for include transformation
f.puts "\n/* ======== #{name} ======== */"
content = strip_include_guard(content, guard) if guard
content = transform_includes(content, inline: true)
f.puts content
end
# ========== Source Generation ==========
def write_source_preamble(f)
f.puts <<~PREAMBLE
/*
** mruby amalgamated source
** Generated from mruby source files
**
** This file is auto-generated. Do not edit directly.
*/
#include "mruby.h"
PREAMBLE
end
def write_internal_headers(f)
f.puts "/* ======== Internal headers ======== */"
# Forward declarations needed for amalgamation
# (functions called before defined due to source file ordering)
# Note: mrb_irep_catch_handler_table is static inline in internal.h, no forward decl needed
f.puts <<~FORWARD
/* Forward declarations for amalgamation */
static void mrb_irep_free(mrb_state *mrb, mrb_irep *irep);
static mrb_value mrb_class_find_path(mrb_state *mrb, struct RClass *c);
static void mrb_method_added(mrb_state *mrb, struct RClass *c, mrb_sym mid);
static void mrb_proc_copy(mrb_state *mrb, struct RProc *a, const struct RProc *b);
static size_t mrb_gc_mark_range(mrb_state *mrb, struct RRange *r);
static void mrb_ary_decref(mrb_state *mrb, mrb_shared_array *shared);
static mrb_int mrb_proc_arity(const struct RProc *p);
FORWARD
# internal.h
internal_path = "#{include_dir}/mruby/internal.h"
if File.exist?(internal_path)
content = File.read(internal_path, mode: "rb")
content = strip_include_guard(content, extract_include_guard(content))
content = transform_source_includes(content)
f.puts "\n/* mruby/internal.h */"
f.puts content
end
# presym/table.h (generated, needed by symbol.c)
table_path = "#{build_include_dir}/mruby/presym/table.h"
if File.exist?(table_path)
content = File.read(table_path, mode: "rb")
content = transform_source_includes(content)
f.puts "\n/* mruby/presym/table.h */"
f.puts content
end
# value_array.h (internal src header)
value_array_path = "#{src_dir}/value_array.h"
if File.exist?(value_array_path)
content = File.read(value_array_path, mode: "rb")
content = strip_include_guard(content, extract_include_guard(content))
content = transform_source_includes(content)
f.puts "\n/* src/value_array.h */"
f.puts content
end
end
def write_core_sources(f)
f.puts "\n/* ======== Core sources ======== */"
CORE_SOURCE_ORDER.each do |source|
path = "#{src_dir}/#{source}"
next unless File.exist?(path)
write_source_content(f, "src/#{source}", path)
end
# Clear potentially conflicting macros from core sources
write_macro_cleanup(f, "core")
end
def write_generated_sources(f)
# mrblib.c - compiled Ruby stdlib
mrblib_path = "#{@build.build_dir}/mrblib/mrblib.c"
if File.exist?(mrblib_path)
write_source_content(f, "mrblib.c", mrblib_path)
end
end
# Macros that may conflict between source files in amalgamation
# These are #undef-ed after each source file within a gem
CONFLICTING_MACROS = %w[
mrb_stat
mrb_lstat
mrb_fstat
lesser
greater
CASE
NEXT
JUMP
CALL
node_type
push
pop
peek
].freeze
def write_gem_sources(f)
f.puts "\n/* ======== Gem sources ======== */"
library_gems.each do |gem|
# Some gems use 'core/' instead of 'src/' (mruby-compiler, mruby-bigint)
source_dirs = ["#{gem.dir}/src", "#{gem.dir}/core"].select { |d| File.directory?(d) }
# Include C sources if the gem has any
unless source_dirs.empty?
sources = source_dirs.flat_map { |d| Dir.glob("#{d}/**/*.c") }.sort
sources.each_with_index do |path, idx|
rel_path = path.sub("#{gem.dir}/", "")
write_source_content(f, "#{gem.name}: #{rel_path}", path)
# Clear macros between source files to avoid conflicts
# (e.g., mrb_stat macro in file.c vs function in file_test.c)
write_macro_cleanup(f, "#{gem.name}/#{File.basename(path)}") if idx < sources.size - 1
end
end
# Gem's compiled mrblib (Ruby-only gems like mruby-enum-ext have this)
gem_mrblib = "#{gem.build_dir}/gem_mrblib.c"
if File.exist?(gem_mrblib)
write_source_content(f, "#{gem.name}: gem_mrblib.c", gem_mrblib)
end
# Gem's init functions (GENERATED_TMP_mrb_*_gem_init/final)
# Required for both C and Ruby-only gems
gem_init = "#{gem.build_dir}/gem_init.c"
if File.exist?(gem_init)
write_source_content(f, "#{gem.name}: gem_init.c", gem_init)
end
# Clear potentially conflicting macros after each gem
write_macro_cleanup(f, gem.name)
end
# gem_init.c - gem registration
gem_init_path = "#{@build.build_dir}/mrbgems/gem_init.c"
if File.exist?(gem_init_path)
write_source_content(f, "gem_init.c", gem_init_path)
end
end
def write_source_content(f, name, path)
return unless File.exist?(path)
content = File.read(path, mode: "rb")
# For source files, comment out all mruby includes (they're in the header)
# and inline local includes (like .cstub files)
source_dir = File.dirname(path)
content = transform_source_includes(content, source_dir)
f.puts "\n/* ======== #{name} ======== */"
f.puts content
end
def write_macro_cleanup(f, gem_name)
f.puts "\n/* Cleanup macros from #{gem_name} to avoid conflicts */"
CONFLICTING_MACROS.each do |macro|
f.puts "#ifdef #{macro}"
f.puts "#undef #{macro}"
f.puts "#endif"
end
end
# X-macro pattern headers that must be inlined every time they're included
# (not commented out) because they expand differently based on macro definitions
XMACRO_HEADERS = %w[
mruby/ops.h
].freeze
def transform_source_includes(content, source_dir = nil)
content.gsub(/^(\s*)(#\s*include\s+([<"])([^>"]+)[>"])/m) do |match|
prefix = $1
include_stmt = $2
quote_type = $3 # < or "
header = $4
# X-macro headers must be inlined every time (not commented out)
# because they expand differently based on surrounding macro definitions
if XMACRO_HEADERS.include?(header)
xmacro_path = "#{include_dir}/#{header}"
if File.exist?(xmacro_path)
xmacro_content = File.read(xmacro_path, mode: "rb")
"#{prefix}/* Inlined X-macro: #{header} */\n#{xmacro_content}"
else
match
end
# Comment out all mruby-related includes and any header already in amalgam
elsif mruby_header?(header) || header == "mruby.h" ||
@processed_headers.include?(header) || @gem_header_names.include?(header)
"#{prefix}// #{include_stmt} - in amalgam header"
elsif source_dir && quote_type == '"' && !header.include?("/")
# Check for local includes like "known_errors_def.cstub"
local_path = "#{source_dir}/#{header}"
if File.exist?(local_path)
local_content = File.read(local_path, mode: "rb")
"#{prefix}/* Inlined: #{header} */\n#{local_content}"
else
match
end
else
match
end
end
end
# ========== Content Transformation ==========
def extract_include_guard(content)
# Match #ifndef GUARD_NAME at start of file (after comments)
if content =~ /\A(?:\/\*.*?\*\/\s*|\/\/[^\n]*\n)*\s*#ifndef\s+(\w+)\s*\n\s*#define\s+\1/m
$1
end
end
def strip_include_guard(content, guard)
return content unless guard
# Remove opening #ifndef GUARD but KEEP #define GUARD (needed for #ifdef checks)
content = content.sub(/\A((?:\/\*.*?\*\/\s*|\/\/[^\n]*\n)*\s*)#ifndef\s+#{guard}\s*\n/m, '\1')
# Remove closing #endif (any comment is ok, guard name may not match exactly)
content = content.sub(/\n#endif\s*(?:\/\*[^*]*\*\/|\/\/[^\n]*)?\s*\z/m, "\n")
content
end
def transform_includes(content, inline: false)
# Match both "#include" and "# include" (preprocessor allows spaces)
# Only match includes at the start of a line (real preprocessor directives)
content.gsub(/^(\s*)(#\s*include\s+[<"]([^>"]+)[>"])/m) do |match|
prefix = $1
include_stmt = $2
header = $3
if already_included?(header)
# Keep original whitespace, comment out the include
"#{prefix}// #{include_stmt} - in amalgam"
elsif inline && mruby_header?(header)
# Recursively inline this header
"#{prefix}#{inline_header(header)}"
else
match
end
end
end
def already_included?(header)
# Check if header was already processed
# Only use end_with? matching for mruby headers to avoid matching
# system headers like <time.h> against mruby/time.h
if mruby_header?(header)
@processed_headers.any? { |h| h == header || h.end_with?("/#{header}") }
else
@processed_headers.include?(header)
end
end
def mruby_header?(header)
# Check if this is an mruby header that should be inlined/transformed
header == "mruby.h" || header.start_with?("mruby/") ||
%w[mrbconf.h boxing_nan.h boxing_word.h boxing_no.h common.h object.h value_array.h].include?(header)
end
def inline_header(header)
# Find the full path for this header
if header == "mruby.h" || header == "mrbconf.h" || header.start_with?("mruby/")
full_header = header
# Check both source include dir and build include dir (for generated headers)
path = "#{include_dir}/#{header}"
path = "#{build_include_dir}/#{header}" unless File.exist?(path)
else
# Relative includes like "boxing_word.h" -> "mruby/boxing_word.h"
full_header = "mruby/#{header}"
path = "#{include_dir}/#{full_header}"
path = "#{build_include_dir}/#{full_header}" unless File.exist?(path)
end
return "/* #{header} - not found */" unless File.exist?(path)
# Mark as processed to avoid infinite recursion
@processed_headers << header
@processed_headers << full_header
content = File.read(path, mode: "rb")
guard = extract_include_guard(content)
if guard && @processed_guards[guard]
return "/* #{header} - already included */"
end
@processed_guards[guard] = true if guard
content = strip_include_guard(content, guard) if guard
content = transform_includes(content, inline: true)
"\n/* ======== #{full_header} (inlined) ======== */\n#{content}"
end
end
end