From 25afdf4a1f7968cc2df3fe9522377dfda903b425 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Sat, 20 Jun 2026 08:26:33 -0400 Subject: [PATCH] fix: remove byref delegate parameters incompatible with Mono/Xamarin (issue #458) TemplateDelegate and DecoratorDelegate previously used `in EncodedTextWriter` (ref readonly) parameters, which caused System.NotImplementedException on Xamarin.iOS and other Mono-based runtimes when compiled via expression trees. Remove the `in` modifier to pass EncodedTextWriter by value; also remove MakeByRefType() in CompilationContext so expression lambdas use plain parameters. Adds regression tests in Issue458Tests.cs. Co-Authored-By: Claude Sonnet 4.6 --- source/Handlebars.Test/ClosureBuilderTests.cs | 2 +- source/Handlebars.Test/DecoratorTests.cs | 2 +- .../Handlebars.Test/Issues/Issue458Tests.cs | 41 +++++++++++++++++++ .../Handlebars/Compiler/CompilationContext.cs | 4 +- source/Handlebars/Compiler/FunctionBuilder.cs | 2 +- .../Handlebars/Compiler/HandlebarsCompiler.cs | 8 ++-- .../Expression/DecoratorDefinition.cs | 6 +-- 7 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 source/Handlebars.Test/Issues/Issue458Tests.cs diff --git a/source/Handlebars.Test/ClosureBuilderTests.cs b/source/Handlebars.Test/ClosureBuilderTests.cs index d3c3858c..6cd02bbc 100644 --- a/source/Handlebars.Test/ClosureBuilderTests.cs +++ b/source/Handlebars.Test/ClosureBuilderTests.cs @@ -123,7 +123,7 @@ private static List GenerateDecoratorDelegates(ClosureBuilder var helpers = new List(); for (int i = 0; i < count; i++) { - DecoratorDelegate helper = (in EncodedTextWriter writer, BindingContext context, TemplateDelegate function) => function; + DecoratorDelegate helper = (EncodedTextWriter writer, BindingContext context, TemplateDelegate function) => function; builder.Add(Const(helper)); helpers.Add(helper); } diff --git a/source/Handlebars.Test/DecoratorTests.cs b/source/Handlebars.Test/DecoratorTests.cs index 81f35713..61d9decb 100644 --- a/source/Handlebars.Test/DecoratorTests.cs +++ b/source/Handlebars.Test/DecoratorTests.cs @@ -46,7 +46,7 @@ public void OverrideFunctionWithDecorator(IHandlebars handlebars) (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) => { var value = arguments.At(0); - return (in EncodedTextWriter writer, BindingContext bindingContext) => + return (EncodedTextWriter writer, BindingContext bindingContext) => { writer.WriteSafeString(value); function(writer, bindingContext); diff --git a/source/Handlebars.Test/Issues/Issue458Tests.cs b/source/Handlebars.Test/Issues/Issue458Tests.cs new file mode 100644 index 00000000..1ad3585e --- /dev/null +++ b/source/Handlebars.Test/Issues/Issue458Tests.cs @@ -0,0 +1,41 @@ +using Xunit; + +namespace HandlebarsDotNet.Test +{ + public class Issue458Tests + { + [Fact] + public void Issue458_BasicCompileAndRender_NoByRefDelegate() + { + // Validates the scenario that fails on Mono when byref delegates are used + var h = Handlebars.Create(); + var render = h.Compile("{{input}}"); + var result = render(new { input = 42 }); + Assert.Equal("42", result); + } + + [Fact] + public void Issue458_BlockHelper_NoByRefDelegate() + { + // Block helpers also exercise TemplateDelegate compilation + var h = Handlebars.Create(); + h.RegisterHelper("loud", (writer, options, context, arguments) => + { + options.Template(writer, context); + }); + var render = h.Compile("{{#loud}}hello{{/loud}}"); + var result = render(new { }); + Assert.Equal("hello", result); + } + + [Fact] + public void Issue458_NestedTemplates_NoByRefDelegate() + { + // Nested template compilation exercises the expression tree lambda paths + var h = Handlebars.Create(); + var render = h.Compile("{{#each items}}{{this}},{{/each}}"); + var result = render(new { items = new[] { "a", "b", "c" } }); + Assert.Equal("a,b,c,", result); + } + } +} diff --git a/source/Handlebars/Compiler/CompilationContext.cs b/source/Handlebars/Compiler/CompilationContext.cs index dc0ec098..b85cd85f 100644 --- a/source/Handlebars/Compiler/CompilationContext.cs +++ b/source/Handlebars/Compiler/CompilationContext.cs @@ -9,7 +9,7 @@ public CompilationContext(ICompiledHandlebarsConfiguration configuration) { Configuration = configuration; BindingContext = Expression.Parameter(typeof(BindingContext), "context"); - EncodedWriter = Expression.Parameter(typeof(EncodedTextWriter).MakeByRefType(), "writer"); + EncodedWriter = Expression.Parameter(typeof(EncodedTextWriter), "writer"); Args = new CompilationContextArgs(this); } @@ -18,7 +18,7 @@ public CompilationContext(CompilationContext context) { Configuration = context.Configuration; BindingContext = Expression.Parameter(typeof(BindingContext), "context"); - EncodedWriter = Expression.Parameter(typeof(EncodedTextWriter).MakeByRefType(), "writer"); + EncodedWriter = Expression.Parameter(typeof(EncodedTextWriter), "writer"); Args = new CompilationContextArgs(this); } diff --git a/source/Handlebars/Compiler/FunctionBuilder.cs b/source/Handlebars/Compiler/FunctionBuilder.cs index 993c8457..981c4ce2 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -11,7 +11,7 @@ namespace HandlebarsDotNet.Compiler internal static class FunctionBuilder { private static readonly TemplateDelegate EmptyTemplateLambda = - (in EncodedTextWriter writer, BindingContext context) => { }; + (EncodedTextWriter writer, BindingContext context) => { }; public static Expression Reduce(Expression expression, CompilationContext context, out IReadOnlyList decorators) { diff --git a/source/Handlebars/Compiler/HandlebarsCompiler.cs b/source/Handlebars/Compiler/HandlebarsCompiler.cs index fbf63a64..0f1be129 100644 --- a/source/Handlebars/Compiler/HandlebarsCompiler.cs +++ b/source/Handlebars/Compiler/HandlebarsCompiler.cs @@ -8,7 +8,7 @@ namespace HandlebarsDotNet.Compiler { - public delegate void TemplateDelegate(in EncodedTextWriter writer, BindingContext context); + public delegate void TemplateDelegate(EncodedTextWriter writer, BindingContext context); internal static class HandlebarsCompiler { @@ -29,7 +29,7 @@ public static TemplateDelegate Compile(ExtendedStringReader source, CompilationC { var a1 = action; var decorator = decorators.Compile(compilationContext); - action = (in EncodedTextWriter writer, BindingContext context) => + action = (EncodedTextWriter writer, BindingContext context) => { decorator(writer, context, a1)(writer, context); }; @@ -63,7 +63,7 @@ internal static TemplateDelegate CompileView(ViewReaderFactory readerFactoryFact { var a1 = compiledView; var decorator = decorators.Compile(compilationContext); - compiledView = (in EncodedTextWriter writer, BindingContext context) => + compiledView = (EncodedTextWriter writer, BindingContext context) => { decorator(writer, context, a1)(writer, context); }; @@ -78,7 +78,7 @@ internal static TemplateDelegate CompileView(ViewReaderFactory readerFactoryFact var compiledLayout = CompileView(readerFactoryFactory, layoutPath, new CompilationContext(compilationContext)); - return (in EncodedTextWriter writer, BindingContext context) => + return (EncodedTextWriter writer, BindingContext context) => { var config = context.Configuration; using var bindingContext = BindingContext.Create(config, null); diff --git a/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs index f0b3821b..85c2f28a 100644 --- a/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs +++ b/source/Handlebars/Compiler/Translation/Expression/DecoratorDefinition.cs @@ -4,7 +4,7 @@ namespace HandlebarsDotNet.Compiler { - public delegate TemplateDelegate DecoratorDelegate(in EncodedTextWriter writer, BindingContext context, TemplateDelegate function); + public delegate TemplateDelegate DecoratorDelegate(EncodedTextWriter writer, BindingContext context, TemplateDelegate function); internal readonly struct DecoratorDefinition { @@ -20,7 +20,7 @@ public DecoratorDefinition(Expression decorator, ExpressionContainer function; + if (Function is null || Decorator is null) return (EncodedTextWriter writer, BindingContext bindingContext, TemplateDelegate function) => function; var lambda = Expression.Lambda( Decorator, @@ -47,7 +47,7 @@ CompilationContext context var definition = decoratorDefinitions[index]; var f = definition.Compile(context); var current = decorator; - decorator = (in EncodedTextWriter writer, BindingContext bindingContext, TemplateDelegate function) => + decorator = (EncodedTextWriter writer, BindingContext bindingContext, TemplateDelegate function) => f(writer, bindingContext, current(writer, bindingContext, function)); }