Skip to content

Make Data configurable #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .yardopts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
--hide-void-return
-
CHANGELOG.md
CONFIG.md
LICENSE.txt
126 changes: 126 additions & 0 deletions CONFIG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Configuration file specification

## Example
```yaml
function:
include_name:
- !ruby/regexp /^rb_/i
- !ruby/regexp /^rstring_/i

exclude_name:
# deprecated functions
- !ruby/regexp /^rb_check_safe_str$/i
- !ruby/regexp /^rb_clear_constant_cache$/i
- !ruby/regexp /^rb_clone_setup$/i

pointer_hint:
RSTRING_PTR:
self: raw
rb_data_object_make:
4: sref

struct:
include_name:
- !ruby/regexp /^rb_/i
- re_registers

exclude_name:
- rb_data_type_struct

type:
include_name:
- !ruby/regexp /^rb_/i
- !ruby/regexp /^st_/i
- ID
- VALUE

exclude_name: []

enum:
include_name:
- ruby_value_type
- rb_io_wait_readwrite

exclude_name: []
```

## Full configuration file
[config/default.yml](config/default.yml) is used by https://github.com/sue445/go-gem-wrapper to generate bindings for Go.

## `function.include_name`, `struct.include_name`, `type.include_name`, `enum.include_name`
Return functions and structures that match the condition with a [RubyHeaderParser::Parser](lib/ruby_header_parser/parser.rb)

e.g.

```yaml
struct:
include_name:
- !ruby/regexp /^rb_/i
- re_registers
```

Elements in the array accept the following

* `String`: Exact match
* `Regexp`(`!ruby/regexp`): Regular expression match

## `function.exclude_name`, `struct.exclude_name`, `type.exclude_name`, `enum.exclude_name`
Doesn't return functions and structures that match the condition with a [RubyHeaderParser::Parser](lib/ruby_header_parser/parser.rb)

e.g.

```yaml
function:
exclude_name:
- !ruby/regexp /^rb_check_safe_str$/i
- rb_scan_args_bad_format
```

`exclude_name` is preferred over `include_name`

Other specifications are the same as `include_name`

## `function.pointer_hint`
Provide a hint if the function argument type is a pointer

e.g.

```yaml
function:
pointer_hint:
RSTRING_PTR: # function name (Exact match)
self: raw
rb_data_object_make:
4: sref
```

### Function arguments (`1`, `2`, `3`, ...)
* `ref` (default)
* normal pointer
* e.g. 1st argument of `VALUE rb_const_list(void*)`
* `in_ref`
* input only pointer
* e.g. 2nd argument of `void rb_define_variable(const char *name, VALUE *var)`
* `sref`
* special one for multiple pointer
* e.g. 3rd argument of `rb_data_typed_object_make(VALUE klass, const rb_data_type_t *type, void **datap, size_t size)`
* `array`
* array
* e.g. 4th argument of `VALUE rb_funcallv(VALUE recv, ID mid, int argc, const VALUE *argv)`
* `ref_array`
* array of pointer
* e.g. 10th argument of `rb_scan_args_set(int kw_flag, int argc, const VALUE *argv, int n_lead, int n_opt, int n_trail, bool f_var, bool f_hash, bool f_block, VALUE *vars[], RB_UNUSED_VAR(const char *fmt), RB_UNUSED_VAR(int varc))`
* `str_array`
* array of string
* e.g. 2nd argument of `int rb_find_file_ext(VALUE *feature, const char *const *exts)`
* `function`
* function pointer
* e.g. 4th argument of `void rb_define_method(VALUE klass, const char *mid, VALUE (*func)(), int arity)`

### Function return value (`self`)
* `ref` (default)
* normal pointer
* e.g. Return value of `int *rb_errno_ptr(void)`
* `raw`
* In many cases `char*` can be interpreted as a string. But if `raw` is specified, it suppresses interpretation as a string
* e.g. Return value of `char* RSTRING_PTR(VALUE str)`
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ See below for details.

https://sue445.github.io/ruby_header_parser/RubyHeaderParser/Parser.html

See [CONFIG.md](CONFIG.md) for config file details

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/ruby_header_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module RubyHeaderParser
class Error < StandardError; end

autoload :ArgumentDefinition, "ruby_header_parser/argument_definition"
autoload :Data, "ruby_header_parser/data"
autoload :Config, "ruby_header_parser/config"
autoload :EnumDefinition, "ruby_header_parser/enum_definition"
autoload :FunctionDefinition, "ruby_header_parser/function_definition"
autoload :Parser, "ruby_header_parser/parser"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# frozen_string_literal: true

module RubyHeaderParser
# Manager for `data.yml`
class Data
# Manager for config file
class Config
# @!attribute [r] data
# @return [Hash]
attr_reader :data

def initialize
yaml = File.read(File.join(__dir__.to_s, "data.yml"))
# @param config_file [String]
#
# @note See [CONFIG.md](../file.CONFIG.html) for config file details
def initialize(config_file)
yaml = File.read(config_file)
@data = YAML.safe_load(yaml, aliases: true, permitted_classes: [Regexp])
end

Expand Down
27 changes: 16 additions & 11 deletions lib/ruby_header_parser/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class Parser # rubocop:disable Metrics/ClassLength
attr_reader :dist_preprocessed_header_file

# @!attribute [r] data
# @return [RubyHeaderParser::Data]
attr_reader :data
# @return [RubyHeaderParser::Config]
attr_reader :config

DEFAULT_HEADER_FILE = "#{RbConfig::CONFIG["rubyhdrdir"]}/ruby.h".freeze

Expand All @@ -30,15 +30,20 @@ class Parser # rubocop:disable Metrics/ClassLength
# @param include_paths [Array<String>]
# @param dist_preprocessed_header_file [String,nil] Destination path to the output of preprocessed ruby.h.
# (default: `"#{Dir.tmpdir}/ruby_preprocessed.h"`)
# @param config_file [String,nil] Path to config file (default: `config/default.yml`)
#
# @note `dist_preprocessed_header_file` is used as the output destination for temporary files when the parser
# is executed
#
# @note See [CONFIG.md](../file.CONFIG.html) for config file details
def initialize(dist_preprocessed_header_file: nil, header_file: DEFAULT_HEADER_FILE,
include_paths: DEFAULT_INCLUDE_PATHS)
include_paths: DEFAULT_INCLUDE_PATHS, config_file: nil)
@header_file = header_file
@include_paths = include_paths
@dist_preprocessed_header_file = dist_preprocessed_header_file || File.join(Dir.tmpdir, "ruby_preprocessed.h")
@data = Data.new

config_file ||= File.expand_path("../../config/default.yml", __dir__.to_s)
@config = Config.new(config_file)
end

# @return [Array<RubyHeaderParser::FunctionDefinition>]
Expand All @@ -59,7 +64,7 @@ def extract_struct_definitions
parts = line.split("\t")

struct_name = parts[0]
next unless data.should_generate_struct?(struct_name)
next unless config.should_generate_struct?(struct_name)

definitions << StructDefinition.new(
name: struct_name,
Expand All @@ -76,7 +81,7 @@ def extract_type_definitions

type_name = parts[0]

next unless data.should_generate_type?(type_name)
next unless config.should_generate_type?(type_name)

definitions << TypeDefinition.new(
name: type_name,
Expand All @@ -97,7 +102,7 @@ def extract_enum_definitions

value = parts[0]

next unless data.should_generate_enum?(enum_name)
next unless config.should_generate_enum?(enum_name)

hash[enum_name] ||= EnumDefinition.new(name: enum_name)
hash[enum_name].values << value
Expand Down Expand Up @@ -131,7 +136,7 @@ def generate_function_definition_from_line(line:, kind:, is_parse_multiline_defi
function_name = parts[0]
filepath = parts[1]

return nil unless data.should_generate_function?(function_name)
return nil unless config.should_generate_function?(function_name)

return nil unless parts[3] == kind

Expand Down Expand Up @@ -228,7 +233,7 @@ def create_typeref(definition:, function_name:, typeref_field:, filepath:, line_
typeref_pointer = nil
if typeref_type.match?(/\*+$/)
typeref_type = typeref_type.gsub(/\*+$/, "").strip
typeref_pointer = data.function_self_pointer_hint(function_name)
typeref_pointer = config.function_self_pointer_hint(function_name)
end

TyperefDefinition.new(type: typeref_type, pointer: typeref_pointer)
Expand Down Expand Up @@ -319,12 +324,12 @@ def analyze_argument_type(function_name:, arg_pos:, parts:)
case original_type
when /\*+$/
type = original_type.gsub(/\*+$/, "").strip
pointer = data.function_arg_pointer_hint(function_name:, pos: arg_pos)
pointer = config.function_arg_pointer_hint(function_name:, pos: arg_pos)

when /^void\s*/, /\(.*\)/
# function pointer (e.g. void *(*func)(void *)) is treated as `void*`
type = "void"
pointer = data.function_arg_pointer_hint(function_name:, pos: arg_pos)
pointer = config.function_arg_pointer_hint(function_name:, pos: arg_pos)

else
type = original_type
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module RubyHeaderParser
class Data
class Config
attr_reader data: Hash[untyped, untyped]

def initialize: () -> void
def initialize: (String config_file) -> void

def function_arg_pointer_hint: (function_name: String, pos: Integer) -> (:ref | :array | :ref_array | :function | :sref | :str_array | :in_ref)

Expand Down
5 changes: 3 additions & 2 deletions sig/ruby_header_parser/parser.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ module RubyHeaderParser
attr_reader header_file: String
attr_reader include_paths: Array[String]
attr_reader dist_preprocessed_header_file: String
attr_reader data: Data
attr_reader config: Config

DEFAULT_HEADER_FILE: String
DEFAULT_INCLUDE_PATHS: Array[String]

def initialize: (
?dist_preprocessed_header_file: String?,
?header_file: String,
?include_paths: Array[String]
?include_paths: Array[String],
?config_file: String?
) -> void

def extract_function_definitions: () -> Array[FunctionDefinition]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# frozen_string_literal: true

RSpec.describe RubyHeaderParser::Data do
let(:data) { RubyHeaderParser::Data.new }
RSpec.describe RubyHeaderParser::Config do
let(:config) { RubyHeaderParser::Config.new(default_config_file) }

describe "#function_arg_pointer_hint" do
subject { data.function_arg_pointer_hint(function_name:, pos:) }
subject { config.function_arg_pointer_hint(function_name:, pos:) }

context "found in data.yml" do
context "found in config/default.yml" do
let(:function_name) { "rb_funcallv" }
let(:pos) { 4 }

it { should eq :array }
end

context "not found in data.yml" do
context "not found in config/default.yml" do
let(:function_name) { "rb_funcallv" }
let(:pos) { 5 }

Expand All @@ -22,23 +22,23 @@
end

describe "#function_self_pointer_hint" do
subject { data.function_self_pointer_hint(function_name) }
subject { config.function_self_pointer_hint(function_name) }

context "found in data.yml" do
context "found in default.yml" do
let(:function_name) { "RSTRING_PTR" }

it { should eq :raw }
end

context "not found in data.yml" do
context "not found in config/default.yml" do
let(:function_name) { "rb_class2name" }

it { should eq :ref }
end
end

describe "#should_generate_function?" do
subject { data.should_generate_function?(function_name) }
subject { config.should_generate_function?(function_name) }

context "rb function (denied)" do
let(:function_name) { "rb_check_safe_str" }
Expand All @@ -60,7 +60,7 @@
end

describe "#should_generate_struct?" do
subject { data.should_generate_struct?(struct_name) }
subject { config.should_generate_struct?(struct_name) }

context "rb struct" do
let(:struct_name) { "rb_random_struct" }
Expand All @@ -76,7 +76,7 @@
end

describe "#should_generate_type?" do
subject { data.should_generate_type?(type_name) }
subject { config.should_generate_type?(type_name) }

context "rb type" do
let(:type_name) { "rb_data_type_t" }
Expand All @@ -98,7 +98,7 @@
end

describe "#should_generate_enum?" do
subject { data.should_generate_enum?(enum_name) }
subject { config.should_generate_enum?(enum_name) }

context "ruby_value_type" do
let(:enum_name) { "ruby_value_type" }
Expand Down
2 changes: 1 addition & 1 deletion spec/ruby_header_parser/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RSpec.describe RubyHeaderParser::Parser do
include_context "uses temp dir"

let(:parser) { RubyHeaderParser::Parser.new(dist_preprocessed_header_file:) }
let(:parser) { RubyHeaderParser::Parser.new(dist_preprocessed_header_file:, config_file: default_config_file) }
let(:dist_preprocessed_header_file) { File.join(temp_dir, "ruby_preprocessed.h") }

describe "#extract_function_definitions" do
Expand Down
5 changes: 5 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@
meta[:aggregate_failures] = true
end
end

# @return [String]
def default_config_file
File.expand_path("../config/default.yml", __dir__)
end