From 4450f741a1a80ff44b34957b25ad520f15eb15ff Mon Sep 17 00:00:00 2001 From: sfaqer Date: Fri, 25 Sep 2026 15:04:50 +1000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B5=D1=88=D0=B5=D0=B2=D0=BB=D0=B5=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=B5=D1=80=D1=82=D0=BA=D0=B0=20=D0=B2=D1=8B=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скомпилированная лямбда вызывает C#-метод напрямую, а не через делегат из замыкания - делегат остался от ручных оберток до перехода на деревья выражений. Для параметров int/decimal/bool/string и возврата bool/int/decimal обертка при построении выбирает отдельные методы преобразования: значение нужного вида берется сразу, без упаковки и перебора типов, остальное идет прежним путем ConvertParam. Так же собираются конструкторы в TypeFactory. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Machine/Contexts/ContextMethodMapper.cs | 54 ++------ .../Contexts/ContextValuesMarshaller.cs | 82 +++++++++++++ src/ScriptEngine/Machine/TypeFactory.cs | 2 +- .../ContextValuesMarshallerTest.cs | 116 ++++++++++++++++++ 4 files changed, 211 insertions(+), 43 deletions(-) create mode 100644 src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs diff --git a/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs b/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs index 0cec69071..cce6144f0 100644 --- a/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs +++ b/src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs @@ -194,7 +194,7 @@ private static ContextCallableDelegate CreateFunction(ContextMethodIn { var methodCall = MethodCallExpression(target, out var instParam, out var argsParam, out var processParam); - var convertRetMethod = ContextValuesMarshaller.BslReturnValueGenericConverter.MakeGenericMethod(target.ReturnType); + var convertRetMethod = ContextValuesMarshaller.GetReturnValueConverter(target.ReturnType); var convertReturnCall = Expression.Call(convertRetMethod, methodCall); var body = convertReturnCall; @@ -224,24 +224,23 @@ private static ContextCallableDelegate CreateProcedure(ContextMethodI return l.Compile(); } - private static InvocationExpression MethodCallExpression( - ContextMethodInfo contextMethod, + private static MethodCallExpression MethodCallExpression( + ContextMethodInfo contextMethod, out ParameterExpression instParam, out ParameterExpression argsParam, out ParameterExpression processParam) { // For those who dare: - // Код ниже формирует следующую лямбду с 2-мя замыканиями realMethodDelegate и defaults: - // (inst, args) => + // Код ниже формирует следующую лямбду: + // (inst, args, process) => // { - // realMethodDelegate(inst, - // ConvertParam(args[i], defaults[i]), + // inst.RealMethod( + // ConvertParam(args[0], default1, process), // ... - // ConvertParam(args[i], defaults[i])); + // ConvertParam(args[N-1], defaultN, process)); // } var target = contextMethod.GetWrappedMethod(); - var methodClojure = CreateDelegateExpr(target); instParam = Expression.Parameter(typeof(TInstance), "inst"); argsParam = Expression.Parameter(typeof(IValue[]), "args"); @@ -250,17 +249,16 @@ private static InvocationExpression MethodCallExpression( var parameters = target.GetParameters(); var (clrIndexStart, argsLen) = contextMethod.InjectsProcess ? (1, parameters.Length - 1) : (0, parameters.Length); - + var argsPass = new List(); - argsPass.Add(instParam); - + if (contextMethod.InjectsProcess) argsPass.Add(processParam); for (int bslIndex = 0,clrIndex = clrIndexStart; bslIndex < argsLen; bslIndex++, clrIndex++) { var targetType = parameters[clrIndex].ParameterType; - var convertMethod = ContextValuesMarshaller.BslGenericParameterConverter.MakeGenericMethod(targetType); + var convertMethod = ContextValuesMarshaller.GetParameterConverter(targetType); Expression defaultArg; if (parameters[clrIndex].HasDefaultValue) @@ -281,35 +279,7 @@ private static InvocationExpression MethodCallExpression( argsPass.Add(Expression.Convert(conversionCall, targetType)); } - var methodCall = Expression.Invoke(methodClojure, argsPass); - return methodCall; - } - - private static Expression CreateDelegateExpr(MethodInfo target) - { - var types = new List(); - types.Add(target.DeclaringType); - types.AddRange(target.GetParameters().Select(x => x.ParameterType)); - Type delegateType; - if (target.ReturnType == typeof(void)) - { - delegateType = Expression.GetActionType(types.ToArray()); - } - else - { - types.Add(target.ReturnType); - delegateType = Expression.GetFuncType(types.ToArray()); - } - - var deleg = target.CreateDelegate(delegateType); - - var delegateExpr = Expression.Constant(deleg); - var conversion = Expression.Convert(delegateExpr, delegateType); - - var delegateCreator = Expression.Lambda(conversion).Compile(); - var methodClojure = Expression.Constant(delegateCreator.DynamicInvoke()); - - return methodClojure; + return Expression.Call(instParam, target, argsPass); } } } diff --git a/src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs b/src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs index b6e5b9c7e..b89f32a9f 100644 --- a/src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs +++ b/src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs @@ -5,6 +5,7 @@ This Source Code Form is subject to the terms of the at http://mozilla.org/MPL/2.0/. ----------------------------------------------------------*/ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; @@ -20,6 +21,21 @@ namespace ScriptEngine.Machine.Contexts { public static class ContextValuesMarshaller { + private static readonly Dictionary _primitiveParameterConverters = new Dictionary + { + [typeof(int)] = GetOwnMethod(nameof(ConvertInt32Param)), + [typeof(decimal)] = GetOwnMethod(nameof(ConvertDecimalParam)), + [typeof(bool)] = GetOwnMethod(nameof(ConvertBooleanParam)), + [typeof(string)] = GetOwnMethod(nameof(ConvertStringParam)), + }; + + private static readonly Dictionary _primitiveReturnValueConverters = new Dictionary + { + [typeof(int)] = GetOwnMethod(nameof(ConvertInt32ReturnValue)), + [typeof(decimal)] = GetOwnMethod(nameof(ConvertDecimalReturnValue)), + [typeof(bool)] = GetOwnMethod(nameof(ConvertBooleanReturnValue)), + }; + public static MethodInfo BslParameterConverter { get; private set; } public static MethodInfo BslGenericParameterConverter { get; private set; } public static MethodInfo BslReturnValueGenericConverter { get; private set; } @@ -37,6 +53,72 @@ static ContextValuesMarshaller() BslReturnValueGenericConverter = typeof(ContextValuesMarshaller).GetMethods() .First(x => x.Name == nameof(ConvertReturnValue) && x.GetGenericArguments().Length == 1); } + + /// + /// Метод преобразования аргумента из Bsl в параметр типа для сгенерированных оберток, + /// с сигнатурой как у ConvertParam<T>(IValue, T, IBslProcess). Для частых типов - отдельные методы + /// без упаковки и перебора типов, выбор делается один раз, при построении обертки. + /// + public static MethodInfo GetParameterConverter(Type type) + { + return _primitiveParameterConverters.TryGetValue(type, out var converter) + ? converter + : BslGenericParameterConverter.MakeGenericMethod(type); + } + + /// + /// Метод преобразования возвращаемого значения C#-метода в значение Bsl для сгенерированных оберток. + /// + public static MethodInfo GetReturnValueConverter(Type type) + { + return _primitiveReturnValueConverters.TryGetValue(type, out var converter) + ? converter + : BslReturnValueGenericConverter.MakeGenericMethod(type); + } + + private static MethodInfo GetOwnMethod(string name) + { + return typeof(ContextValuesMarshaller).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static); + } + + // Результат тот же, что у ConvertParam: значение нужного вида берется сразу, остальное - общим путем + private static int ConvertInt32Param(IValue value, int defaultValue, IBslProcess process) + { + return value is BslNumericValue number ? ToInt32(number) : ConvertParam(value, defaultValue, process); + } + + private static decimal ConvertDecimalParam(IValue value, decimal defaultValue, IBslProcess process) + { + return value is BslNumericValue number ? (decimal)number : ConvertParam(value, defaultValue, process); + } + + private static bool ConvertBooleanParam(IValue value, bool defaultValue, IBslProcess process) + { + return value is BslBooleanValue boolean ? (bool)boolean : ConvertParam(value, defaultValue, process); + } + + private static string ConvertStringParam(IValue value, string defaultValue, IBslProcess process) + { + return value is BslStringValue str ? (string)str : ConvertParam(value, defaultValue, process); + } + + private static int ToInt32(BslNumericValue number) + { + try + { + return (int)(decimal)number; + } + catch (OverflowException) + { + throw RuntimeException.InvalidArgumentValue(); + } + } + + private static IValue ConvertBooleanReturnValue(bool value) => ValueFactory.Create(value); + + private static IValue ConvertInt32ReturnValue(int value) => ValueFactory.Create(value); + + private static IValue ConvertDecimalReturnValue(decimal value) => ValueFactory.Create(value); /// /// Выполняет конвертацию значения из Bsl в значение параметра метода C# diff --git a/src/ScriptEngine/Machine/TypeFactory.cs b/src/ScriptEngine/Machine/TypeFactory.cs index 6bef446da..93c2401f6 100644 --- a/src/ScriptEngine/Machine/TypeFactory.cs +++ b/src/ScriptEngine/Machine/TypeFactory.cs @@ -122,7 +122,7 @@ private InstanceConstructor CreateConstructor(IValue[] arguments) { var conversionArg = Expression.ArrayIndex(argsParam, Expression.Constant(i)); var targetType = parameters[paramIndex].ParameterType; - var convertMethod = ContextValuesMarshaller.BslGenericParameterConverter.MakeGenericMethod(targetType); + var convertMethod = ContextValuesMarshaller.GetParameterConverter(targetType); Expression marshalledArg; if (parameters[paramIndex].HasDefaultValue) diff --git a/src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs b/src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs new file mode 100644 index 000000000..5765bc6f4 --- /dev/null +++ b/src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs @@ -0,0 +1,116 @@ +/*---------------------------------------------------------- +This Source Code Form is subject to the terms of the +Mozilla Public License, v.2.0. If a copy of the MPL +was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. +----------------------------------------------------------*/ + +using FluentAssertions; +using OneScript.Contexts; +using OneScript.Exceptions; +using OneScript.Execution; +using OneScript.Values; +using ScriptEngine.Machine; +using ScriptEngine.Machine.Contexts; +using Xunit; + +namespace OneScript.Core.Tests +{ + public class ContextValuesMarshallerTest + { + [Fact] + public void ConvertParam_Int() + { + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(5), 0).Should().Be(5); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create("12"), 0).Should().Be(12); + ContextValuesMarshaller.ConvertParam(Variable.Create(ValueFactory.Create(3), "x"), 0).Should().Be(3); + ContextValuesMarshaller.ConvertParam(BslSkippedParameterValue.Instance, 7).Should().Be(7); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(), 7).Should().Be(7); + } + + [Fact] + public void ConvertParam_Int_Overflow() + { + var huge = ValueFactory.Create(100_000_000_000m); + + Assert.Throws(() => ContextValuesMarshaller.ConvertParam(huge, 0)); + } + + [Fact] + public void ConvertParam_Primitives() + { + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(1.5m), 0m).Should().Be(1.5m); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(true), false).Should().BeTrue(); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(1), false).Should().BeTrue(); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create("строка"), "").Should().Be("строка"); + ContextValuesMarshaller.ConvertParam(ValueFactory.Create(5), "").Should().Be("5"); + } + + [Fact] + public void ConvertReturnValue_Primitives() + { + ContextValuesMarshaller.ConvertReturnValue(true).Should().BeSameAs(BslBooleanValue.True); + ContextValuesMarshaller.ConvertReturnValue(5).AsNumber().Should().Be(5); + ContextValuesMarshaller.ConvertReturnValue(1.5m).AsNumber().Should().Be(1.5m); + } + + [Fact] + public void ContextMethod_CallThroughWrapper() + { + Call("Повторить", ValueFactory.Create("ab"), ValueFactory.Create(2)).AsString(ForbiddenBslProcess.Instance).Should().Be("abab"); + Call("Повторить", ValueFactory.Create(5), BslSkippedParameterValue.Instance).AsString(ForbiddenBslProcess.Instance).Should().Be("5"); + + Call("Удвоить", ValueFactory.Create(5)).AsNumber().Should().Be(10); + Call("Удвоить", ValueFactory.Create("6")).AsNumber().Should().Be(12); + Call("Удвоить", Variable.Create(ValueFactory.Create(3), "x")).AsNumber().Should().Be(6); + Call("Удвоить", ValueFactory.Create()).AsNumber().Should().Be(14); + Call("Удвоить", BslSkippedParameterValue.Instance).AsNumber().Should().Be(14); + + Call("Половина", ValueFactory.Create(3m)).AsNumber().Should().Be(1.5m); + + Call("Не", ValueFactory.Create(true)).Should().BeSameAs(BslBooleanValue.False); + Call("Не", ValueFactory.Create(0)).Should().BeSameAs(BslBooleanValue.True); + } + + [Fact] + public void ContextMethod_CallThroughWrapper_IntOverflow() + { + Assert.Throws(() => Call("Удвоить", ValueFactory.Create(100_000_000_000m))); + } + + private static IValue Call(string methodName, params IValue[] arguments) + { + var context = new MarshallingTestContext(); + context.CallAsFunction(context.GetMethodNumber(methodName), arguments, out var result, ForbiddenBslProcess.Instance); + return result; + } + + [ContextClass("ТестМаршаллинга", "MarshallingTest")] + private class MarshallingTestContext : AutoContext + { + [ContextMethod("Повторить", "Repeat")] + public string Repeat(string text, int count = 1) + { + return string.Concat(System.Linq.Enumerable.Repeat(text, count)); + } + + [ContextMethod("Удвоить", "Twice")] + public int Twice(int value = 7) + { + return value * 2; + } + + [ContextMethod("Половина", "Half")] + public decimal Half(decimal value) + { + return value / 2; + } + + [ContextMethod("Не", "Not")] + public bool Not(bool value) + { + return !value; + } + } + } +}