掘金 后端 ( ) • 2024-04-17 16:34

IOTSharp的启动与其脚本引擎部分研究

项目地址:https://github.com/IoTSharp/IoTSharp/tree/master/IoTSharp.Interpreter

为什么要研究这个

IoTSharp是一个开源的物联网平台,用于数据收集、处理、可视化和设备管理。项目中,需要做基础设施模块,脚本引擎部分,参考他的脚本引擎设计

IOTSharp 容器的启动

根据官方文档

这个目录下的 docker-compose.yml、appsettings.Production.json 复制出来

启动

然后进入http://localhost:8086 来配置 influxDb,创建一个 token

然后修改appsettings.Production.json

"TelemetryStorage":"http://influx:8086/?org=iotsharp&bucket=iotsharp-bucket&token=iotsharp-token&&latest=-72h",

重启 IotSharp 容器

docker restart iotsharp

Chrome浏览器访问 http://localhost:2927/ 来注册账号

使用邮箱+密码登录

IOTSharp 脚本引擎部分源码分析

位于源代码,IoTSharp.Interpreter 文件夹下

说明:这里的脚本应该使用了 对象.属性名 的形式作为变量,所以传来一个对象即可。项目中脚本变量是单独的 变量名 (脚本需要前端配置的,这样方便配置脚本),就需要对每个变量单独 SetValue,而不是只 Set 一个对象。

ScriptEngineBase 基类,Do 方法是虚方法,参数 input 是一个 json,需要后续反序列化为ExpandoObject 对象

public class ScriptEngineBase
{
    internal   CancellationToken _cancellationToken;
    internal readonly ILogger _logger;
    internal readonly EngineSetting _setting;

    public ScriptEngineBase(ILogger logger, EngineSetting setting, CancellationToken cancellationToken)
    {
        _cancellationToken = cancellationToken;
        _logger = logger;
        _setting = setting;
    }
    public virtual void UseCancellationToken(CancellationToken cancellation)
    {
        _cancellationToken = cancellation;
    }
    public virtual string Do(string _source, string input)
    {
        return input;
    }
}

BASICScriptEngine 默认引擎

JSON字符串转换成一个可以在运行时动态操作其成员的对象,并将这个对象再转换回JSON字符串

_source 是脚本,input 是脚本中的参数

public class BASICScriptEngine : ScriptEngineBase
{
  
    public BASICScriptEngine(ILogger<PythonScriptEngine> logger  , IOptions<EngineSetting> _opt) : base(logger, _opt.Value, System.Threading.Tasks.Task.Factory.CancellationToken)
    {

    }
    public override string Do(string _source, string input)
    {
        var expConverter = new ExpandoObjectConverter();
        dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(input, expConverter);
        //https://github.com/Timu5/BasicSharp
       var outputjson=   JsonConvert.SerializeObject(obj);
        return outputjson;
    }
}

实现类, JavaScriptEngine

public class JavaScriptEngine:ScriptEngineBase, IDisposable
{
    private  Engine _engine;
    private  JsonParser _parser;
    private bool disposedValue;
    public JavaScriptEngine(ILogger<JavaScriptEngine> logger, IOptions<EngineSetting> _opt):base(logger,_opt.Value, Task.Factory.CancellationToken)
    {
        var engine = new Engine(options =>
        {

            // Limit memory allocations to MB
            options.LimitMemory(4_000_000);

            // Set a timeout to 4 seconds.
            options.TimeoutInterval(TimeSpan.FromSeconds(_opt.Value.Timeout));

            // Set limit of 1000 executed statements.
            // options.MaxStatements(1000);
            // Use a cancellation token.
            options.CancellationToken(_cancellationToken);
        });
        _engine = engine;
        _parser = new JsonParser(_engine);
    }


    public  override string    Do(string _source,string input)
    {
       var js = _engine.SetValue("input",_parser.Parse(input)).Evaluate(_source).ToObject();
        var json= System.Text.Json.JsonSerializer.Serialize(js);
        _logger.LogDebug($"source:{Environment.NewLine}{ _source}{Environment.NewLine}{Environment.NewLine}input:{Environment.NewLine}{ input}{Environment.NewLine}{Environment.NewLine} ouput:{Environment.NewLine}{ json}{Environment.NewLine}{Environment.NewLine}");
        return json;
    }

    protected virtual void Dispose(bool disposing)
    {
       
        if (!disposedValue)
        {
            if (disposing)
            {
                _engine= null;
                _parser = null;
                // TODO: 释放托管状态(托管对象)
            }

            // TODO: 释放未托管的资源(未托管的对象)并重写终结器
            // TODO: 将大型字段设置为 null
            disposedValue = true;
        }
    }



    public void Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

EngineSetting 配置类

public class EngineSetting
{
    public double Timeout { get; set; } = 4;
}

ScriptEnginesExtensions 类

定义了添加脚本引擎的扩展方法

public static class ScriptEnginesExtensions
{
    public static IServiceCollection AddScriptEngines(this IServiceCollection services, IConfiguration  configuration)
    {
        services.AddTransient<JavaScriptEngine>();
        services.AddTransient<PythonScriptEngine>();
        services.AddTransient<SQLEngine>();
        services.AddTransient<LuaScriptEngine>();
        services.AddTransient<CSharpScriptEngine>();
        services.Configure<EngineSetting>(configuration);
        return services;
    }
}

示例 1: 调用 jsEngine

[TestClass]
    public class ScriptEngineTest
    {
        private JavaScriptEngine _js_engine;
        private PythonScriptEngine _python_engine;
        private LuaScriptEngine _lua_engine;
        private CScriptEngine _c_engine;
        private SQLEngine _sql_engine;
        private CSharpScriptEngine _csharp_engine;

        [TestInitialize]
        public void InitTestScriptEngine()
        {
            var lgf = LoggerFactory.Create(f =>
                                           {
                                               f.AddConsole();
                                           });


            _js_engine = new JavaScriptEngine(lgf.CreateLogger<JavaScriptEngine>(), Options.Create( new Interpreter.EngineSetting() { Timeout = 4 }));
            _python_engine = new PythonScriptEngine(lgf.CreateLogger<PythonScriptEngine>(), Options.Create(new Interpreter.EngineSetting() { Timeout = 4 }));
            _lua_engine = new  LuaScriptEngine (lgf.CreateLogger<LuaScriptEngine>(), Options.Create(new Interpreter.EngineSetting() { Timeout = 4 }));
            _c_engine = new CScriptEngine(lgf.CreateLogger<CScriptEngine>(), Options.Create(new Interpreter.EngineSetting() { Timeout = 4 }));
            _sql_engine=new SQLEngine(lgf.CreateLogger<SQLEngine>(), Options.Create(new Interpreter.EngineSetting() { Timeout = 4 }));
            _csharp_engine = new  CSharpScriptEngine   (lgf.CreateLogger<CSharpScriptEngine>(), Options.Create(new Interpreter.EngineSetting() { Timeout = 4 }), new MemoryCache(new MemoryCacheOptions()));
        }
        [TestMethod]
        public void TestJavaScript()
        {
            var intput = System.Text.Json.JsonSerializer.Serialize(new { temperature = 39, height = 192, weight = 121 });

            string output = _js_engine.Do(@"
var _m = (input.height / 100);
var output = {
    fever: input.temperature > 38 ? true : false,
    fat: input.weight / (_m * _m)>28?true:false
};
return output;
", intput);

            var t = new { fever = true, fat = true };
            var outpuobj = System.Text.Json.JsonSerializer.Deserialize(output, t.GetType());
            Assert.AreEqual(outpuobj, t);
        }

示例 2: 调用 jsEngine

public class FlowOperation
{
    [Key]

    public Guid OperationId { get; set; }
    public DateTime? AddDate { get; set; }
    /// <summary>
    /// 节点处理状态,0 创建完
    /// </summary>
    public int NodeStatus { get; set; }  
    public string OperationDesc { get; set; }
    public string Data  { get; set; }
    public string BizId { get; set; }
    public string bpmnid { get; set; }
    public Flow Flow { get; set; }
    public FlowRule FlowRule { get; set; }
    public BaseEvent BaseEvent { get; set; }
    public int Step { get; set; }
    public string Tag { get; set; }
}

var taskoperation = new FlowOperation()
{
    OperationId = Guid.NewGuid(),
    bpmnid = flow.bpmnid,
    AddDate = DateTime.UtcNow,
    FlowRule = peroperation.BaseEvent.FlowRule,
    Flow = flow,
    Data = JsonConvert.SerializeObject(data),
    NodeStatus = 1,
    OperationDesc = "Run" + flow.NodeProcessScriptType + "Task:" + flow.Flowname,
    Step = step,
    BaseEvent = peroperation.BaseEvent
};

case "javascript":
    {

        using (var js = _sp.GetRequiredService<JavaScriptEngine>())
        {
            try
            {
                string result = js.Do(scriptsrc, taskoperation.Data);
                obj = JsonConvert.DeserializeObject<object>(result);

            }
            catch (Exception ex)
            {

                _logger.Log(LogLevel.Warning, "javascript脚本执行异常");
                taskoperation.OperationDesc += ex.Message;
                taskoperation.NodeStatus = 2;
            }

        }
    }
    break;

脚本引擎选型与基本使用

目标

将脚本存在数据库中,取出脚本,传递参数进行计算,来达到不用写死脚本在代码中的效果

可以指定计算引擎类型,比如 javascript 的 jint、C# Python 等

方案选择

  1. NCalc:一个灵活的数学表达式计算器,支持.NET环境,允许运行时解析和执行字符串形式的表达式。
  2. Jint:一个JavaScript解释器,可以在.NET环境中执行JavaScript代码,适合需要执行复杂逻辑的场景。
  3. Roslyn:.NET的编译器平台,支持运行时代码生成和编译,可以用于动态编译并执行C#代码。

Roslyn 使用示例

using Microsoft.CodeAnalysis.CSharp.Scripting;

var code = "1 + 2";
var result = await CSharpScript.EvaluateAsync<int>(code);
Console.WriteLine(result); // 输出: 3

如果需要传递参数

using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

// 假设这是从数据库中读取到的计算公式
string formula = "a + b";
// 创建一个包含计算所需参数的全局对象
var globals = new Globals { a = 1, b = 2 };

// 执行计算
var result = await CSharpScript.EvaluateAsync<int>(formula, globals: globals);

Console.WriteLine(result); // 输出: 3

// 定义一个包含参数的类
public class Globals
{
    public int a;
    public int b;
}

Ncalc 使用示例

using NCalc;

// 从数据库中获取的公式字符串
string formula = "3 + 2 * [Parameter1]";
// 创建参数字典
Dictionary<string, object> parameters = new Dictionary<string, object>
{
    { "Parameter1", 10 }
};

Expression expression = new Expression(formula);
// 设置参数
foreach(var param in parameters)
{
    expression.Parameters[param.Key] = param.Value;
}

// 执行计算
object result = expression.Evaluate();
Console.WriteLine(result); // 输出:23

Jint 使用示例

using Jint;

var engine = new Engine()
    .SetValue("参数1", 10); // 设置JavaScript变量

var result = engine.Execute("var 结果 = 3 + 2 * 参数1; 结果").GetCompletionValue().AsNumber();

Console.WriteLine(result); // 输出: 23

Jint 用法最简单,采用 Jint 实现。