Skip to content
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
54 changes: 12 additions & 42 deletions src/ScriptEngine/Machine/Contexts/ContextMethodMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ private static ContextCallableDelegate<TInstance> 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;

Expand Down Expand Up @@ -224,24 +224,23 @@ private static ContextCallableDelegate<TInstance> 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<TypeOfArg1>(args[i], defaults[i]),
// inst.RealMethod(
// ConvertParam<TypeOfArg1>(args[0], default1, process),
// ...
// ConvertParam<TypeOfArgN>(args[i], defaults[i]));
// ConvertParam<TypeOfArgN>(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");
Expand All @@ -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<Expression>();
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)
Expand All @@ -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<Type>();
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);
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions src/ScriptEngine/Machine/Contexts/ContextValuesMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +21,21 @@ namespace ScriptEngine.Machine.Contexts
{
public static class ContextValuesMarshaller
{
private static readonly Dictionary<Type, MethodInfo> _primitiveParameterConverters = new Dictionary<Type, MethodInfo>
{
[typeof(int)] = GetOwnMethod(nameof(ConvertInt32Param)),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

обычный switch не будет тут выгоднее Dictionary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Выбор делается один раз — при построении обёртки метода, а не на вызове, так что скорость тут не важна. Словарь — ради точного совпадения типа: switch по Type.GetTypeCode отнёс бы к int и перечисления с базовым int, а цепочка if по typeof вышла бы длиннее. Если читается хуже — переделаю на if.

[typeof(decimal)] = GetOwnMethod(nameof(ConvertDecimalParam)),
[typeof(bool)] = GetOwnMethod(nameof(ConvertBooleanParam)),
[typeof(string)] = GetOwnMethod(nameof(ConvertStringParam)),
};

private static readonly Dictionary<Type, MethodInfo> _primitiveReturnValueConverters = new Dictionary<Type, MethodInfo>
{
[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; }
Expand All @@ -37,6 +53,72 @@ static ContextValuesMarshaller()
BslReturnValueGenericConverter = typeof(ContextValuesMarshaller).GetMethods()
.First(x => x.Name == nameof(ConvertReturnValue) && x.GetGenericArguments().Length == 1);
}

/// <summary>
/// Метод преобразования аргумента из Bsl в параметр типа <paramref name="type"/> для сгенерированных оберток,
/// с сигнатурой как у ConvertParam&lt;T&gt;(IValue, T, IBslProcess). Для частых типов - отдельные методы
/// без упаковки и перебора типов, выбор делается один раз, при построении обертки.
/// </summary>
public static MethodInfo GetParameterConverter(Type type)
{
return _primitiveParameterConverters.TryGetValue(type, out var converter)
? converter
: BslGenericParameterConverter.MakeGenericMethod(type);
}

/// <summary>
/// Метод преобразования возвращаемого значения C#-метода в значение Bsl для сгенерированных оберток.
/// </summary>
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<T>: значение нужного вида берется сразу, остальное - общим путем
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);

/// <summary>
/// Выполняет конвертацию значения из Bsl в значение параметра метода C#
Expand Down
2 changes: 1 addition & 1 deletion src/ScriptEngine/Machine/TypeFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
116 changes: 116 additions & 0 deletions src/Tests/OneScript.Core.Tests/ContextValuesMarshallerTest.cs
Original file line number Diff line number Diff line change
@@ -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<RuntimeException>(() => 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<RuntimeException>(() => 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<MarshallingTestContext>
{
[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;
}
}
}
}