From ab0abcb483916cd87bdd5529d162215576adb027 Mon Sep 17 00:00:00 2001 From: Rex Morgan Date: Fri, 19 Jun 2026 20:58:16 -0400 Subject: [PATCH] fix: remove byref (in) from delegate parameters incompatible with Mono/Xamarin (#458) Mono's AOT/JIT compiler does not support delegates with byref parameters (ref/in). TemplateDelegate and DecoratorDelegate used `in EncodedTextWriter` which caused System.NotImplementedException: byref delegate at runtime on Xamarin.iOS and other Mono-based platforms. - Change `TemplateDelegate` from `delegate void TemplateDelegate(in EncodedTextWriter, ...)` to pass the struct by value - Change `DecoratorDelegate` similarly - Update `CompilationContext.EncodedWriter` to use `typeof(EncodedTextWriter)` instead of `MakeByRefType()` so Expression.Lambda no longer generates a byref parameter - Update all lambda literals in HandlebarsCompiler, FunctionBuilder, DecoratorDefinition and tests to drop the `in` modifier EncodedTextWriter is a small readonly struct (3 reference-type fields) so the by-value copy is cheap and there is no observable behaviour change on standard .NET runtimes. Adds two regression tests: Issue458_BasicTemplateCompilationAndRender and Issue458_BlockHelperTemplateCompilationAndRender. Co-Authored-By: Claude Sonnet 4.6 --- source/Handlebars.Test/ClosureBuilderTests.cs | 2 +- source/Handlebars.Test/DecoratorTests.cs | 2 +- source/Handlebars.Test/IssueTests.cs | 27 +++++++++++++++++++ .../Handlebars/Compiler/CompilationContext.cs | 4 +-- source/Handlebars/Compiler/FunctionBuilder.cs | 4 +-- .../Handlebars/Compiler/HandlebarsCompiler.cs | 8 +++--- .../Expression/DecoratorDefinition.cs | 6 ++--- 7 files changed, 40 insertions(+), 13 deletions(-) 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/IssueTests.cs b/source/Handlebars.Test/IssueTests.cs index 31136117..5e41a2fd 100644 --- a/source/Handlebars.Test/IssueTests.cs +++ b/source/Handlebars.Test/IssueTests.cs @@ -733,5 +733,32 @@ public void UnrecognisedExpressionThrowsOutOfMemoryException() Assert.Throws(()=> Handlebars.Compile(source)); } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/458 + // System.NotImplementedException: byref delegate on Xamarin.iOS / Mono + // The Mono runtime does not support delegates with byref (in/ref) parameters. + // TemplateDelegate previously used `in EncodedTextWriter` which produced a + // byref delegate incompatible with Mono's AOT/JIT compiler. + [Fact] + public void Issue458_BasicTemplateCompilationAndRender() + { + // Validates the scenario that fails on Mono: simple compile + render + var handlebars = Handlebars.Create(); + var render = handlebars.Compile("{{input}}"); + object data = new { input = 42 }; + var actual = render(data); + Assert.Equal("42", actual); + } + + // Issue: https://github.com/Handlebars-Net/Handlebars.Net/issues/458 + // Ensures block helpers still work after the byref delegate fix + [Fact] + public void Issue458_BlockHelperTemplateCompilationAndRender() + { + var handlebars = Handlebars.Create(); + var render = handlebars.Compile("{{#if show}}visible{{/if}}"); + var actual = render(new { show = true }); + Assert.Equal("visible", actual); + } } } \ No newline at end of file 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..15bf96f6 100644 --- a/source/Handlebars/Compiler/FunctionBuilder.cs +++ b/source/Handlebars/Compiler/FunctionBuilder.cs @@ -10,8 +10,8 @@ namespace HandlebarsDotNet.Compiler { internal static class FunctionBuilder { - private static readonly TemplateDelegate EmptyTemplateLambda = - (in EncodedTextWriter writer, BindingContext context) => { }; + private static readonly TemplateDelegate EmptyTemplateLambda = + (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)); }