diff --git a/spec/bindgen/processor/extern_c_spec.cr b/spec/bindgen/processor/extern_c_spec.cr index c5c6db6..b9807ce 100644 --- a/spec/bindgen/processor/extern_c_spec.cr +++ b/spec/bindgen/processor/extern_c_spec.cr @@ -13,8 +13,8 @@ describe Bindgen::Processor::ExternC do doc = Bindgen::Parser::Document.new db = Bindgen::TypeDatabase.new(Bindgen::TypeDatabase::Configuration.new, "boehmgc-cpp") - db.add("HasToCpp", to_cpp: "TO_CPP", copy_structure: true) - db.add("HasFromCpp", from_cpp: "FROM_CPP", copy_structure: true) + db.add("HasToCpp", to_cpp: Bindgen::Template.from_string("TO_CPP"), copy_structure: true) + db.add("HasFromCpp", from_cpp: Bindgen::Template.from_string("FROM_CPP"), copy_structure: true) db.add("PassByValue", pass_by: Bindgen::TypeDatabase::PassBy::Reference) extern_c_void_func = Bindgen::Parser::Method.new( diff --git a/spec/bindgen/template_spec.cr b/spec/bindgen/template_spec.cr new file mode 100644 index 0000000..2cdefc2 --- /dev/null +++ b/spec/bindgen/template_spec.cr @@ -0,0 +1,95 @@ +require "../spec_helper" + +describe "Template" do + describe ".from_string" do + it "constructs the no-op template from nil" do + Bindgen::Template.from_string(nil).should be_a(Bindgen::Template::None) + end + + it "can construct a simple template from a string" do + Bindgen::Template.from_string("%x", simple: true).should eq( + Bindgen::Template::Basic.new("%x", simple: true)) + end + + it "can construct a full template from a string" do + expected = Bindgen::Template::Basic.new("%x") + Bindgen::Template.from_string("%x").should eq(expected) + Bindgen::Template.from_string("%x", simple: false).should eq(expected) + end + end + + describe "#no_op?" do + it "returns true for the no-op template" do + Bindgen::Template::None.new.no_op?.should be_true + end + + it "returns true for string templates with only `%`" do + Bindgen::Template::Basic.new("%").no_op?.should be_true + Bindgen::Template::Basic.new("%", simple: true).no_op?.should be_true + end + + it "returns false for any other templates" do + conversion1 = Bindgen::Template::Basic.new("%x") + conversion2 = Bindgen::Template::Basic.new("%x", simple: true) + conversion3 = Bindgen::Template::Sequence.new(conversion1, conversion2) + + conversion1.no_op?.should be_false + conversion2.no_op?.should be_false + conversion3.no_op?.should be_false + end + end + + describe "#followed_by" do + it "composes two templates" do + conversion1 = Bindgen::Template::Basic.new("%x") + conversion2 = Bindgen::Template::Basic.new("%y") + conversion3 = Bindgen::Template::Sequence.new(conversion1, conversion2) + + conversion1.followed_by(conversion2).should eq(conversion3) + end + + it "is #no_op? only when both templates are #no_op?" do + op = Bindgen::Template::Basic.new("%x") + no = Bindgen::Template::None.new + + op.followed_by(op).no_op?.should be_false + op.followed_by(no).no_op?.should be_false + no.followed_by(op).no_op?.should be_false + no.followed_by(no).no_op?.should be_true + end + end + + describe "None#template" do + it "returns the code unmodified" do + Bindgen::Template::None.new.template("123").should eq("123") + end + end + + describe "Basic#template" do + it "substitutes % with the supplied code" do + Bindgen::Template::Basic.new("a%b%c").template("123").should eq("a123b123c") + end + + it "may access environment variables if it is a full template" do + key, value = ENV.first + Bindgen::Template::Basic.new("%{#{key}}%").template("123").should eq("123#{value}123") + end + + it "substitutes %% with % if it is a simple template" do + Bindgen::Template::Basic.new("%%a%%%b%%%%c", simple: true).template("123").should eq("%a%123b%%c") + end + end + + describe "Sequence#template" do + it "composes multiple templates" do + a_conv = Bindgen::Template::Basic.new("%_a") + b_conv = Bindgen::Template::Basic.new("%_b") + + conversion = Bindgen::Template::Sequence.new(a_conv, b_conv) + conversion.template("c").should eq("c_a_b") + + conversion = Bindgen::Template::Sequence.new(a_conv, a_conv, a_conv, b_conv, b_conv, a_conv) + conversion.template("c").should eq("c_a_a_a_b_b_a") + end + end +end diff --git a/src/bindgen/call.cr b/src/bindgen/call.cr index 99f1431..791782c 100644 --- a/src/bindgen/call.cr +++ b/src/bindgen/call.cr @@ -37,18 +37,21 @@ module Bindgen # Call result type configuration. class Result < Expression - # Conversion template (`Util.template`) to get the data out of the method, - # ready to be returned back. - getter conversion : String? + # Conversion template to get the data out of the method, ready to be + # returned back. + getter conversion : Template::Base - def initialize(@type, @type_name, @reference, @pointer, @conversion, @nilable = false) + def initialize(@type, @type_name, @reference, @pointer, @conversion = Template::None.new, @nilable = false) + end + + # Applies the result's conversion template to a piece of code. + def apply_conversion(code : String) : String + conversion.template(code) end # Converts the result into an argument of *name*. def to_argument(name : String, default = nil) : Argument - call = name - templ = @conversion # Conversion template - call = Util.template(templ, name) if templ + call = apply_conversion(name) Argument.new( type: @type, @@ -67,9 +70,7 @@ module Bindgen class ProcResult < Result # Converts the result into an argument of *name*. def to_argument(name : String, block = false) : Argument - call = name - templ = @conversion # Conversion template - call = Util.template(templ, name) if templ + call = apply_conversion(name) ProcArgument.new( block: block, diff --git a/src/bindgen/call_builder/cpp_call.cr b/src/bindgen/call_builder/cpp_call.cr index a4ea7f8..79252b1 100644 --- a/src/bindgen/call_builder/cpp_call.cr +++ b/src/bindgen/call_builder/cpp_call.cr @@ -25,12 +25,7 @@ module Bindgen def to_code(call : Call, _platform : Graph::Platform) : String pass_args = call.arguments.map(&.call).join(", ") code = %[#{call.name}(#{pass_args})] - - if templ = call.result.conversion - code = Util.template(templ, code) - end - - code + call.result.apply_conversion(code) end end end diff --git a/src/bindgen/call_builder/cpp_qobject_connect.cr b/src/bindgen/call_builder/cpp_qobject_connect.cr index ed0078f..5d8b17b 100644 --- a/src/bindgen/call_builder/cpp_qobject_connect.cr +++ b/src/bindgen/call_builder/cpp_qobject_connect.cr @@ -28,12 +28,7 @@ module Bindgen inner = @proc.body.to_code(@proc, platform) code = %[QObject::connect(_self_, (#{ptr})&#{call.name}, [_proc_](#{lambda_args}){ #{inner}; })] - - if templ = call.result.conversion - code = Util.template(templ, code) - end - - code + call.result.apply_conversion(code) end end end diff --git a/src/bindgen/call_builder/cpp_to_crystal_proc.cr b/src/bindgen/call_builder/cpp_to_crystal_proc.cr index 7626299..41f3c23 100644 --- a/src/bindgen/call_builder/cpp_to_crystal_proc.cr +++ b/src/bindgen/call_builder/cpp_to_crystal_proc.cr @@ -26,12 +26,7 @@ module Bindgen def to_code(call : Call, platform : Graph::Platform) : String pass_args = call.arguments.map(&.call).join(", ") code = %[#{call.name}(#{pass_args})] - - if templ = call.result.conversion - code = Util.template(templ, code) - end - - code + call.result.apply_conversion(code) end end end diff --git a/src/bindgen/call_builder/crystal_binding.cr b/src/bindgen/call_builder/crystal_binding.cr index 54753b8..3943954 100644 --- a/src/bindgen/call_builder/crystal_binding.cr +++ b/src/bindgen/call_builder/crystal_binding.cr @@ -64,11 +64,7 @@ module Bindgen post = @post_hook pass_args = call.arguments.map(&.call).join(", ") - code = %[Binding.#{call.name}(#{pass_args})] - - if templ = call.result.conversion - code = Util.template(templ, code) - end + code = call.result.apply_conversion %[Binding.#{call.name}(#{pass_args})] # Support for pre- and post hooks. String.build do |b| diff --git a/src/bindgen/call_builder/crystal_from_cpp.cr b/src/bindgen/call_builder/crystal_from_cpp.cr index ed4ee67..7a67755 100644 --- a/src/bindgen/call_builder/crystal_from_cpp.cr +++ b/src/bindgen/call_builder/crystal_from_cpp.cr @@ -6,7 +6,12 @@ module Bindgen end # Calls the *method*, using the *proc_name* to call-through to Crystal. - def build(method : Parser::Method, receiver = "self") : Call + # + # If `do_block` is true, the generated `Proc` expression will use + # `do ... end` instead of `{ ... }`. This allows embedding the code body + # inside a string conversion template, without the code block being + # interpreted as an environment variable (see also `Template::Basic`). + def build(method : Parser::Method, receiver = "self", do_block = false) : Call pass = Crystal::Pass.new(@db) argument = Crystal::Argument.new(@db) @@ -26,32 +31,25 @@ module Bindgen name: method.crystal_name, result: result, arguments: arguments, - body: Body.new(@db, receiver), + body: Body.new(@db, receiver, do_block), ) end # Combines the results *outer* to *inner*. private def combine_result(outer, inner) - conv_out = outer.conversion - conv_in = inner.conversion - - if conv_out && conv_in - conversion = Util.template(conv_out, conv_in) - else - conversion = conv_out || conv_in - end + combined_conversion = inner.conversion.followed_by(outer.conversion) Call::Result.new( type: outer.type, type_name: outer.type_name, pointer: outer.pointer, reference: outer.reference, - conversion: conversion, + conversion: combined_conversion, ) end class Body < Call::Body - def initialize(@db : TypeDatabase, @receiver : String) + def initialize(@db : TypeDatabase, @receiver : String, @do_block : Bool) end def to_code(call : Call, platform : Graph::Platform) : String @@ -67,12 +65,12 @@ module Bindgen block_arg_names = call.arguments.map(&.name).join(", ") block_args = "|#{block_arg_names}|" unless pass_args.empty? - body = "#{@receiver}.#{call.name}(#{pass_args})" - if templ = call.result.conversion - body = Util.template(templ, body) + body = call.result.apply_conversion "#{@receiver}.#{call.name}(#{pass_args})" + if @do_block + %[Proc(#{proc_args}).new do #{block_args} #{body} end] + else + %[Proc(#{proc_args}).new{#{block_args} #{body} }] end - - %[Proc(#{proc_args}).new{#{block_args} #{body} }] end end end diff --git a/src/bindgen/call_builder/crystal_superclass.cr b/src/bindgen/call_builder/crystal_superclass.cr index b07fcb9..8d04b99 100644 --- a/src/bindgen/call_builder/crystal_superclass.cr +++ b/src/bindgen/call_builder/crystal_superclass.cr @@ -14,7 +14,6 @@ module Bindgen type_name: superklass.name, reference: false, pointer: 0, - conversion: nil, ) Call.new( diff --git a/src/bindgen/call_builder/crystal_superclass_init.cr b/src/bindgen/call_builder/crystal_superclass_init.cr index d22f91c..8fd2045 100644 --- a/src/bindgen/call_builder/crystal_superclass_init.cr +++ b/src/bindgen/call_builder/crystal_superclass_init.cr @@ -13,7 +13,6 @@ module Bindgen type_name: method.name, reference: false, pointer: 0, - conversion: nil, ) Call.new( diff --git a/src/bindgen/call_builder/crystal_wrapper.cr b/src/bindgen/call_builder/crystal_wrapper.cr index f0e5bca..e2ab47e 100644 --- a/src/bindgen/call_builder/crystal_wrapper.cr +++ b/src/bindgen/call_builder/crystal_wrapper.cr @@ -87,11 +87,7 @@ module Bindgen class MethodBody < Body def encapsulate(call, code) - if templ = call.result.conversion - Util.template(templ, code) - else - code - end + call.result.apply_conversion(code) end end diff --git a/src/bindgen/cpp/cookbook.cr b/src/bindgen/cpp/cookbook.cr index e9f4ab4..b3d88f6 100644 --- a/src/bindgen/cpp/cookbook.cr +++ b/src/bindgen/cpp/cookbook.cr @@ -8,8 +8,8 @@ module Bindgen # setting. See `Configuration#platform` for the code, and `TEMPLATE.yml` # for user-facing documentation. # - # The functions all return a templating string, to be used with - # `Util.template`. If no conversion is necessary, they return `nil`. + # The functions all return a `Template::Basic` when conversion is + # necessary, and `Template::None` otherwise. abstract class Cookbook # Finds and creates a `Cookbook` by name. def self.create_by_name(name) : Cookbook @@ -27,10 +27,10 @@ module Bindgen end end - # Finds the template (if any) to pass *type* as-is as *pass_by*. The - # returned template takes an expression returning something of *type* and - # turns it into something that can be *pass_by*'d on. - def find(type : Parser::Type, pass_by : TypeDatabase::PassBy) : String? + # Finds the template to pass *type* as-is as *pass_by*. The returned + # template takes an expression returning something of *type* and turns it + # into something that can be *pass_by*'d on. + def find(type : Parser::Type, pass_by : TypeDatabase::PassBy) : Template::Base is_ref = type.reference? is_ptr = !is_ref && type.pointer > 0 @@ -39,8 +39,8 @@ module Bindgen # Same, but provides an override of *type*s reference and pointer # qualification. - def find(base_name : String, is_reference, is_pointer, pass_by : TypeDatabase::PassBy) : String? - case pass_by + def find(base_name : String, is_reference, is_pointer, pass_by : TypeDatabase::PassBy) : Template::Base + template_string = case pass_by when .original? nil # No conversion required. when .reference? @@ -68,6 +68,8 @@ module Bindgen nil end end + + Template.from_string(template_string, simple: true) end # Provides a template to convert a value to a pointer. diff --git a/src/bindgen/cpp/pass.cr b/src/bindgen/cpp/pass.cr index bf419f7..cd565b2 100644 --- a/src/bindgen/cpp/pass.cr +++ b/src/bindgen/cpp/pass.cr @@ -77,6 +77,8 @@ module Bindgen ptr = 0 end + template = Template::None.new + if rules = @db[type]? template = rules.to_cpp type_name = rules.cpp_type || type_name @@ -84,7 +86,7 @@ module Bindgen is_ref, ptr = reconfigure_pass_type(pass_by, is_ref, ptr) end - if template.nil? + if template.no_op? pass_by = type_config_to_pass_by(is_ref, ptr) if pass_by.original? template = conversion_template(pass_by, type, type_name) end @@ -139,6 +141,8 @@ module Bindgen pass_by = TypeDatabase::PassBy::Pointer end + template = Template::None.new + if rules = @db[type]? template = rules.from_cpp type_name = rules.cpp_type || type_name @@ -146,7 +150,7 @@ module Bindgen is_ref, ptr = reconfigure_pass_type(pass_by, is_ref, ptr) end - if template.nil? + if template.no_op? pass_by = type_config_to_pass_by(is_ref, ptr) if pass_by.original? template = conversion_template(pass_by, type, type_name) end @@ -196,7 +200,7 @@ module Bindgen # Finds the conversion template to go from *type* to the desired target # type configuration. - private def conversion_template(pass_by, type, type_name) : String? + private def conversion_template(pass_by, type, type_name) : Template::Base original_ref = type.reference? original_ptr = type_pointer_depth(type) > 0 @@ -213,7 +217,6 @@ module Bindgen type_name: type_name, reference: type.reference?, pointer: type_pointer_depth(type), - conversion: nil, ) end end diff --git a/src/bindgen/crystal/pass.cr b/src/bindgen/crystal/pass.cr index da69ab9..2bcbef2 100644 --- a/src/bindgen/crystal/pass.cr +++ b/src/bindgen/crystal/pass.cr @@ -55,9 +55,12 @@ module Bindgen type_name, in_lib = typer.binding(type) type_name = typer.qualified(type_name, in_lib) if qualified + template = Template::None.new + if rules = @db[type]? template = type_template(rules.converter, rules.from_crystal, "wrap") - template ||= "%.to_unsafe" if to_unsafe && !rules.builtin && !type.builtin? + template = Template.from_string("%.to_unsafe", simple: true) if + template.no_op? && to_unsafe && !rules.builtin && !type.builtin? is_ref, ptr = reconfigure_pass_type(rules.pass_by, is_ref, ptr) end @@ -99,7 +102,7 @@ module Bindgen ptr += 1 if is_ref # Translate reference to pointer is_ref = false - {is_ref, ptr, type_name, nil, nilable} + {is_ref, ptr, type_name, Template::None.new, nilable} end end @@ -146,6 +149,8 @@ module Bindgen type_name, _ = typer.binding(type) end + template = Template::None.new + if rules = @db[type]? unless is_constructor template = type_template(rules.converter, rules.to_crystal, "unwrap") @@ -165,6 +170,8 @@ module Bindgen local_type_name, in_lib = typer.wrapper(type) type_name = typer.qualified(local_type_name, in_lib) + template = Template::None.new + if rules = @db[type]? if rules.kind.class? ptr -= 1 @@ -175,7 +182,7 @@ module Bindgen nilable = false end - if !rules.builtin && !is_constructor && !rules.converter && !rules.to_crystal && !in_lib && !rules.kind.enum? + if !rules.builtin && !is_constructor && !rules.converter && rules.to_crystal.no_op? && !in_lib && !rules.kind.enum? template = wrapper_initialize_template(rules, type_name, nilable) end @@ -226,9 +233,9 @@ module Bindgen # *converter* is set by the user as `converter:` field in the type # configuration, while *translator* is influenced by `to_crystal:` or # `from_crystal:`. - private def type_template(converter, translator, conv_name) + private def type_template(converter, translator, conv_name) : Template::Base if converter - "#{converter}.#{conv_name}(%)" + Template.from_string "#{converter}.#{conv_name}(%)", simple: true else translator end @@ -244,11 +251,13 @@ module Bindgen end end - if nilable + template_string = if nilable %[%.try {|ptr| #{type_name}.new(unwrap: ptr) unless ptr.null?}] else "#{type_name}.new(unwrap: %)" end + + Template.from_string template_string, simple: true end end end diff --git a/src/bindgen/crystal/typename.cr b/src/bindgen/crystal/typename.cr index 5fd0663..b493e2f 100644 --- a/src/bindgen/crystal/typename.cr +++ b/src/bindgen/crystal/typename.cr @@ -50,8 +50,11 @@ module Bindgen rules = @db[type]? return {type.base_name, !type.builtin?} if rules.nil? - in_lib = rules.copy_structure # Copied structures end up in Binding - in_lib ||= !rules.kind.enum? && !rules.builtin && !type.builtin? + # Copied structures end up in Binding + in_lib = rules.copy_structure + # The `Void` check is required for `InstantiateContainers`, which marks + # their binding types as built-in + in_lib ||= !rules.kind.enum? && rules.binding_type != "Void" && !type.builtin? if name = rules.lib_type {name, in_lib} diff --git a/src/bindgen/processor/crystal_binding.cr b/src/bindgen/processor/crystal_binding.cr index b9e0b83..43e6a93 100644 --- a/src/bindgen/processor/crystal_binding.cr +++ b/src/bindgen/processor/crystal_binding.cr @@ -11,7 +11,6 @@ module Bindgen type_name: "Void", reference: false, pointer: 0, - conversion: nil, nilable: false, ) diff --git a/src/bindgen/processor/crystal_wrapper.cr b/src/bindgen/processor/crystal_wrapper.cr index b306dab..dc616be 100644 --- a/src/bindgen/processor/crystal_wrapper.cr +++ b/src/bindgen/processor/crystal_wrapper.cr @@ -42,7 +42,6 @@ module Bindgen type_name: type_name, pointer: type.pointer, reference: false, - conversion: nil, ) end diff --git a/src/bindgen/processor/extern_c.cr b/src/bindgen/processor/extern_c.cr index 0e87b62..c181786 100644 --- a/src/bindgen/processor/extern_c.cr +++ b/src/bindgen/processor/extern_c.cr @@ -35,11 +35,11 @@ module Bindgen # Conversion checks pass = Cpp::Pass.new(@db) any_arg_uses_conversion = method.origin.arguments.any? do |arg| - pass.to_cpp(arg).conversion != nil + !pass.to_cpp(arg).conversion.no_op? end - return if pass.to_crystal(method.origin.return_type).conversion != nil # Rule 3 - return if any_arg_uses_conversion # Rule 2 + return if !pass.to_crystal(method.origin.return_type).conversion.no_op? # Rule 3 + return if any_arg_uses_conversion # Rule 2 # If we end up here, the method can be bound to directly. method.set_tag(Graph::Method::EXPLICIT_BIND_TAG, method.origin.name) diff --git a/src/bindgen/processor/instance_properties.cr b/src/bindgen/processor/instance_properties.cr index 2debaa5..00e31a8 100644 --- a/src/bindgen/processor/instance_properties.cr +++ b/src/bindgen/processor/instance_properties.cr @@ -76,12 +76,7 @@ module Bindgen private class GetterBody < Call::Body def to_code(call : Call, _platform : Graph::Platform) : String code = call.name - - if templ = call.result.conversion - code = Util.template(templ, code) - end - - code + call.result.apply_conversion(code) end end @@ -116,12 +111,7 @@ module Bindgen private class SetterBody < Call::Body def to_code(call : Call, _platform : Graph::Platform) : String code = call.arguments.first.call - - if templ = call.result.conversion - code = Util.template(templ, code) - end - - "#{call.name} = #{code}" + "#{call.name} = #{call.result.apply_conversion code}" end end end diff --git a/src/bindgen/processor/instantiate_containers.cr b/src/bindgen/processor/instantiate_containers.cr index cd01473..4e3cfbf 100644 --- a/src/bindgen/processor/instantiate_containers.cr +++ b/src/bindgen/processor/instantiate_containers.cr @@ -87,11 +87,10 @@ module Bindgen type_name: type.full_name, reference: false, pointer: 0, - conversion: nil, ) Graph::Alias.new( # Build the `typedef`. -origin: origin, + origin: origin, name: klass.name, parent: host, ) @@ -111,10 +110,21 @@ origin: origin, rules.binding_type = "Void" rules.crystal_type ||= "Enumerable(#{result.type_name})" rules.cpp_type ||= cpp_type_name - rules.to_crystal ||= "#{klass.name}.new(unwrap: %)" - rules.from_crystal ||= "BindgenHelper.wrap_container(#{klass.name}, %).to_unsafe" - rules.from_cpp ||= @db.cookbook.value_to_pointer(klass.name) - rules.to_cpp ||= @db.cookbook.pointer_to_reference(klass.name) + + if rules.to_crystal.no_op? + rules.to_crystal = Template.from_string( + "#{klass.name}.new(unwrap: %)", simple: true) + end + if rules.from_crystal.no_op? + rules.from_crystal = Template.from_string( + "BindgenHelper.wrap_container(#{klass.name}, %).to_unsafe", simple: true) + end + if rules.from_cpp.no_op? + rules.from_cpp = Template.from_string(@db.cookbook.value_to_pointer(klass.name)) + end + if rules.to_cpp.no_op? + rules.to_cpp = Template.from_string(@db.cookbook.pointer_to_reference(klass.name)) + end end # Name of *container* with *instance* for diagnostic purposes. diff --git a/src/bindgen/processor/virtual_override.cr b/src/bindgen/processor/virtual_override.cr index fabbdba..850fa4a 100644 --- a/src/bindgen/processor/virtual_override.cr +++ b/src/bindgen/processor/virtual_override.cr @@ -302,7 +302,6 @@ module Bindgen type_name: pass.crystal_proc_name(proc_type), reference: false, pointer: 0, - conversion: nil, ) end @@ -316,7 +315,6 @@ module Bindgen type_name: proc_type.base_name, reference: false, pointer: 0, - conversion: nil, ) end diff --git a/src/bindgen/template.cr b/src/bindgen/template.cr new file mode 100644 index 0000000..e85d739 --- /dev/null +++ b/src/bindgen/template.cr @@ -0,0 +1,18 @@ +require "./template/*" + +module Bindgen + # Conversion templates for `Call::Result`. They govern how a call result's + # code should be transformed to become usable under a certain platform. + module Template + # Constructs a template from the string *pattern*. No-op if the *pattern* + # is `nil`. If *simple* is true, the resulting template does not support + # environment variables. + def self.from_string(pattern : String?, *, simple = false) : Base + if pattern.nil? + None.new + else + Basic.new(pattern, simple: simple) + end + end + end +end diff --git a/src/bindgen/template/base.cr b/src/bindgen/template/base.cr new file mode 100644 index 0000000..e289dee --- /dev/null +++ b/src/bindgen/template/base.cr @@ -0,0 +1,20 @@ +module Bindgen + module Template + # Base class of the templates. + abstract class Base + # Performs a template substitution. + abstract def template(code : String) : String + + # Is this a no-operation template? + abstract def no_op? : Bool + + # Combines two templates into one. The *other* template is run after the + # current template. + def followed_by(other : Base) : Base + return other if no_op? + return self if other.no_op? + Sequence.new(self, other) + end + end + end +end diff --git a/src/bindgen/template/basic.cr b/src/bindgen/template/basic.cr new file mode 100644 index 0000000..c5e8ecb --- /dev/null +++ b/src/bindgen/template/basic.cr @@ -0,0 +1,32 @@ +module Bindgen + module Template + # The default template. Uses `Util.template` to substitute *code* into the + # given *pattern*. + # + # If *simple* is given, the template will only use a subset of the features; + # `%` performs substitution, whereas the escape sequence `%%` outputs a + # literal percent sign. + class Basic < Base + def initialize(@pattern : String, @simple = false) + end + + def no_op? : Bool + @pattern == "%" + end + + def template(code) : String + if @simple + @pattern.gsub(/%+/) do |m| + literals = "%" * (m.size // 2) + out_code = code if m.size % 2 == 1 + "#{literals}#{out_code}" + end + else + Util.template(@pattern, code) + end + end + + def_equals @pattern, @simple + end + end +end diff --git a/src/bindgen/template/none.cr b/src/bindgen/template/none.cr new file mode 100644 index 0000000..0de2b6b --- /dev/null +++ b/src/bindgen/template/none.cr @@ -0,0 +1,18 @@ +module Bindgen + module Template + # The no-op template that returns *code* unmodified. + class None < Base + def no_op? : Bool + true + end + + def template(code) : String + code + end + + def ==(_other : self) + true + end + end + end +end diff --git a/src/bindgen/template/proc_from_wrapper.cr b/src/bindgen/template/proc_from_wrapper.cr new file mode 100644 index 0000000..0d94436 --- /dev/null +++ b/src/bindgen/template/proc_from_wrapper.cr @@ -0,0 +1,33 @@ +module Bindgen + module Template + # Custom template to transform `Proc`s for Crystal wrapper types into `Proc` + # expressions for binding types. + class ProcFromWrapper < Base + def initialize(@proc_type : Parser::Type, @db : TypeDatabase) + end + + def no_op? : Bool + false + end + + def template(code) : String + args = @proc_type.template.not_nil!.arguments + func_args = args[1..-1].map_with_index do |type, i| + Parser::Argument.new("arg#{i}", type) + end + proc_call = Parser::Method.build( + name: "call", + return_type: args.first, + arguments: func_args, + class_name: "Proc", + ) + + builder = CallBuilder::CrystalFromCpp.new(@db) + call = builder.build(proc_call, receiver: "_proc_", do_block: true) + call.body.to_code(call, Graph::Platform::Crystal) + end + + def_equals @proc_type, @db + end + end +end diff --git a/src/bindgen/template/sequence.cr b/src/bindgen/template/sequence.cr new file mode 100644 index 0000000..74d1371 --- /dev/null +++ b/src/bindgen/template/sequence.cr @@ -0,0 +1,36 @@ +module Bindgen + module Template + # Compound template which runs *code* through multiple child templates, in + # the order they are given in the constructor. + class Sequence < Base + getter children = Array(Base).new + getter? no_op : Bool = true + + def initialize(*conversions : Base) + conversions.each do |conversion| + # Flatten `Sequence` templates automatically. + if conversion.is_a?(Sequence) + @children.concat(conversion.children) + else + @children << conversion + end + + # `Sequence` templates returned by `#followed_by` are by construction + # never no-op, but we'll compute this in case `Sequence` is manually + # created. + @no_op &&= conversion.no_op? + end + end + + def template(code) : String + @children.each do |conversion| + code = conversion.template(code) + end + + code + end + + def_equals @children + end + end +end diff --git a/src/bindgen/type_database.cr b/src/bindgen/type_database.cr index b2996c8..5e209d1 100644 --- a/src/bindgen/type_database.cr +++ b/src/bindgen/type_database.cr @@ -18,6 +18,13 @@ module Bindgen end end + # YAML converter for building a conversion template from a string. + module ConversionTemplateConverter + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Template::Base + Template.from_string(Union(String, Nil).new(ctx, node), simple: false) + end + end + # Configuration of instance variables. class InstanceVariableConfig include YAML::Serializable @@ -70,20 +77,36 @@ module Bindgen binding_type: {type: String, nilable: true}, # Template code ran to turn the real C++ type into the crystal type. - from_cpp: {type: String, nilable: true}, + from_cpp: { + type: Template::Base, + default: Template::None.new, + converter: ConversionTemplateConverter, + }, # Template code ran to turn the crystal type into the real C++ type. - to_cpp: {type: String, nilable: true}, + to_cpp: { + type: Template::Base, + default: Template::None.new, + converter: ConversionTemplateConverter, + }, # Converter for this type in Crystal. Takes precedence over the # `#to_crystal` and `#from_crystal` fields. converter: {type: String, nilable: true}, # Template code ran to turn the binding type to Crystal. - to_crystal: {type: String, nilable: true}, + to_crystal: { + type: Template::Base, + default: Template::None.new, + converter: ConversionTemplateConverter, + }, # Template code ran to turn the Crystal type for the binding. - from_crystal: {type: String, nilable: true}, + from_crystal: { + type: Template::Base, + default: Template::None.new, + converter: ConversionTemplateConverter, + }, # How to pass this type to C++? pass_by: { @@ -164,8 +187,8 @@ module Bindgen def initialize( @crystal_type = nil, @cpp_type = nil, @binding_type = nil, - @from_cpp = nil, @to_cpp = nil, @converter = nil, - @from_crystal = nil, @to_crystal = nil, + @from_cpp = Template::None.new, @to_cpp = Template::None.new, @converter = nil, + @from_crystal = Template::None.new, @to_crystal = Template::None.new, @kind = Parser::Type::Kind::Class, @ignore = false, @pass_by = PassBy::Original, @wrapper_pass_by = nil, @sub_class = true, @copy_structure = false, @generate_wrapper = true, diff --git a/src/bindgen/type_helper.cr b/src/bindgen/type_helper.cr index bdbbd6e..35a598c 100644 --- a/src/bindgen/type_helper.cr +++ b/src/bindgen/type_helper.cr @@ -21,7 +21,6 @@ module Bindgen type_name: type.base_name, reference: type.reference?, pointer: type_pointer_depth(type), - conversion: nil, ) end diff --git a/src/bindgen/util.cr b/src/bindgen/util.cr index f4a547c..1785ee7 100644 --- a/src/bindgen/util.cr +++ b/src/bindgen/util.cr @@ -33,8 +33,8 @@ module Bindgen # `NAME` is unset, can be provided through a pipe symbol: `{NAME|default}`. # # It's possible to fall back to the character expansion: `{NAME|%}` - def self.template(haystack : String, replacement : String? = nil, env = ENV) - haystack.gsub(/(%)|{([^}|]+)(?:\|([^}]+))?}/) do |_, match| + def self.template(haystack : String, replacement : String? = nil, env = ENV) : String + haystack.gsub(/(%)|\{([^}|]+)(?:\|([^}]+))?\}/) do |_, match| expansion = match[1]? env_var = match[2]? alternative = match[3]?