Jint
Jint是一个适用于.NET的JavaScript解释器,支持.NET Standard 2.0和.NET 4.6.2(及更高版本)目标,可以在任何现代.NET平台上运行。
使用场景和用户
- 在您的.NET应用程序中安全沙箱环境中运行JavaScript
- 将原生.NET对象和函数暴露给您的JavaScript代码(获取数据库查询结果为JSON,调用.NET方法等)
- 支持在.NET应用程序中进行脚本编写,允许用户使用JavaScript自定义您的应用程序(如Unity游戏)
Jint的一些用户包括 RavenDB、 EventStore、 OrchardCore、 ELSA Workflows、 docfx、 JavaScript Engine Switcher 等等。
支持的功能
ECMAScript 2015 (ES6)
- ✔ ArrayBuffer
- ✔ 箭头函数表达式
- ✔ 二进制和八进制字面量
- ✔ 类支持
- ✔ DataView
- ✔ 解构
- ✔ 默认参数、rest参数和展开运算符
- ✔ 增强的对象字面量
- ✔
for...of
- ❌ 生成器
- ✔ 模板字符串
- ✔ 变量的词法作用域(let和const)
- ✔ Map和Set
- ✔ 模块和模块加载器
- ✔ Promise(实验性,API不稳定)
- ✔ Reflect
- ✔ 代理
- ✔ Symbol
- ❌ 尾调用
- ✔ 类型化数组
- ✔ Unicode
- ✔ Weakmap和Weakset
ECMAScript 2016
- ✔
Array.prototype.includes
- ✔
await
、async
- ✔ 变量和函数的块级作用域
- ✔ 指数运算符
**
- ✔ 解构模式(变量)
ECMAScript 2017
- ✔
Object.values
、Object.entries
和Object.getOwnPropertyDescriptors
- ❌ 共享内存和原子操作
ECMAScript 2018
- ✔
Promise.prototype.finally
- ✔ 正则表达式命名捕获组
- ✔ 对象字面量的rest/spread运算符(
...identifier
) - ✔ SharedArrayBuffer
ECMAScript 2019
- ✔
Array.prototype.flat
、Array.prototype.flatMap
- ✔
String.prototype.trimStart
、String.prototype.trimEnd
- ✔
Object.fromEntries
- ✔
Symbol.description
- ✔ 可选的catch绑定
ECMAScript 2020
- ✔
BigInt
- ✔
export * as ns from
- ✔
for-in
增强 - ✔
globalThis
对象 - ✔
import
- ✔
import.meta
- ✔ 空值合并运算符(
??
) - ✔ 可选链
- ✔
Promise.allSettled
- ✔
String.prototype.matchAll
ECMAScript 2021
- ✔ 逻辑赋值运算符(
&&=
||=
??=
) - ✔ 数字分隔符(
1_000
) - ✔
AggregateError
- ✔
Promise.any
- ✔
String.prototype.replaceAll
- ✔
WeakRef
- ✔
FinalizationRegistry
ECMAScript 2022
- ✔ 类字段
- ✔ 正则表达式匹配索引
- ✔ 顶层await
- ✔ 私有字段的人体工程学品牌检查
- ✔
.at()
- ✔ 可访问的
Object.prototype.hasOwnProperty
(Object.hasOwn
) - ✔ 类静态块
- ✔ 错误原因
ECMAScript 2023
- ✔ 数组从末尾查找
- ✔ 通过复制更改数组
- ✔ Hashbang语法
- ✔ Symbol作为WeakMap键
ECMAScript 2024
- ✔ ArrayBuffer增强 -
ArrayBuffer.prototype.resize
和ArrayBuffer.prototype.transfer
- ❌
Atomics.waitAsync
- ✔ 确保字符串格式正确 -
String.prototype.ensureWellFormed
和String.prototype.isWellFormed
- ✔ 同步可迭代对象分组 -
Object.groupBy
和Map.groupBy
- ✔
Promise.withResolvers
- ❌ 正则表达式标志
/v
ECMAScript Stage 3(尚未确定版本)
- ✔ Float16Array(需要NET 6或更高版本)
- ✔ Import属性
- ✔ JSON模块
- ✔
Promise.try
- ✔ Set方法(
intersection
、union
、difference
、symmetricDifference
、isSubsetOf
、isSupersetOf
、isDisjointFrom
) - ✔ ShadowRealm
- ✔ Uint8Array与base64互转
其他
- 进一步完善的.NET CLR互操作能力
- 执行约束(递归、内存使用、持续时间)
性能
- 由于Jint既不生成任何.NET字节码也不使用DLR,它可以非常快速地运行相对较小的脚本
- 如果您重复运行相同的脚本,应该缓存由Esprima生成的
Script
或Module
实例,而不是内容字符串 - 您应该优先在严格模式下运行引擎,这可以提高性能
您可以查看引擎比较结果,请记住每个用例都不同,基准测试可能无法反映您的实际使用情况。
讨论
加入Gitter聊天,或在stackoverflow上使用jint
标签提问。
视频
这里有一个简短的视频,展示了Jint的工作原理和一些示例用法
https://docs.microsoft.com/shows/code-conversations/sebastien-ros-on-jint-javascript-interpreter-net
线程安全
引擎实例不是线程安全的,不应同时从多个线程访问它们。
示例
这个例子定义了一个名为log
的新值,指向Console.WriteLine
,然后运行一个调用log('Hello World!')
的脚本。
var engine = new Engine()
.SetValue("log", new Action<object>(Console.WriteLine));
engine.Execute(@"
function hello() {
log('Hello World');
};
hello();
");
在这里,变量x
被设置为3
,并在JavaScript中计算x * x
。结果直接返回给.NET,在这种情况下作为double
值9
。
var square = new Engine()
.SetValue("x", 3) // 定义一个新变量
.Evaluate("x * x") // 计算一个语句
.ToObject(); // 将值转换为.NET对象
您还可以直接传递POCO或匿名对象,并从JavaScript中使用它们。例如,在这个例子中,一个新的Person
实例从JavaScript中被操作。
var p = new Person {
Name = "Mickey Mouse"
};
var engine = new Engine()
.SetValue("p", p)
.Execute("p.Name = 'Minnie'");
Assert.AreEqual("Minnie", p.Name);
您可以调用JavaScript函数引用
var result = new Engine()
.Execute("function add(a, b) { return a + b; }")
.Invoke("add",1, 2); // -> 3
或直接通过名称调用
var engine = new Engine()
.Execute("function add(a, b) { return a + b; }");
engine.Invoke("add", 1, 2); // -> 3
访问.NET程序集和类
您可以通过配置引擎实例来允许引擎访问任何.NET类,如下所示:
var engine = new Engine(cfg => cfg.AllowClr());
然后,您可以将System
命名空间作为全局值访问。以下是在命令行实用程序上下文中使用它的方式:
jint> var file = new System.IO.StreamWriter('log.txt');
jint> file.WriteLine('Hello World !');
jint> file.Dispose();
甚至可以为常用的.NET方法创建快捷方式
jint> var log = System.Console.WriteLine;
jint> log('Hello World !');
=> "Hello World !"
当允许CLR时,您可以选择性地传递自定义程序集以加载类型。
var engine = new Engine(cfg => cfg
.AllowClr(typeof(Bar).Assembly)
);
然后,要以与System
相同的方式分配本地命名空间,请使用importNamespace
jint> var Foo = importNamespace('Foo');
jint> var bar = new Foo.Bar();
jint> log(bar.ToString());
添加特定CLR类型引用可以这样做
engine.SetValue("TheType", TypeReference.CreateTypeReference<TheType>(engine));
并以这种方式使用
jint> var o = new TheType();
也支持泛型类型。以下是如何声明、实例化和使用List<string>
:
jint> var ListOfString = System.Collections.Generic.List(System.String);
jint> var list = new ListOfString();
jint> list.Add('foo');
jint> list.Add(1); // 自动转换为String
jint> list.Count; // 2
国际化
如果您不想使用计算机的默认值,可以强制引擎在使用本地JavaScript方法时使用特定的时区或文化。
这个例子强制时区为太平洋标准时间。
var PST = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var engine = new Engine(cfg => cfg.LocalTimeZone(PST));
engine.Execute("new Date().toString()"); // Wed Dec 31 1969 16:00:00 GMT-08:00
本例使用法语作为默认区域性。
var FR = CultureInfo.GetCultureInfo("fr-FR");
var engine = new Engine(cfg => cfg.Culture(FR));
engine.Execute("new Number(1.23).toString()"); // 1.23
engine.Execute("new Number(1.23).toLocaleString()"); // 1,23
执行约束
执行约束用于在脚本执行期间确保满足资源消耗方面的要求,例如:
- 脚本不应使用超过 X 内存。
- 脚本应该只运行最长时间。
你可以通过以下选项配置它们:
var engine = new Engine(options => {
// 将内存分配限制为 4 MB
options.LimitMemory(4_000_000);
// 设置 4 秒的超时。
options.TimeoutInterval(TimeSpan.FromSeconds(4));
// 设置执行语句的限制为 1000 条。
options.MaxStatements(1000);
// 使用取消令牌。
options.CancellationToken(cancellationToken);
}
你也可以通过派生 Constraint
基类来编写自定义约束:
public abstract class Constraint
{
/// 在脚本运行之前调用,当你使用一个引擎对象进行多次执行时非常有用。
public abstract void Reset();
// 在每个语句之前调用,以检查是否满足你的要求;如果不满足 - 抛出异常。
public abstract void Check();
}
例如,我们可以编写一个约束,当 CPU 使用率过高时停止脚本:
class MyCPUConstraint : Constraint
{
public override void Reset()
{
}
public override void Check()
{
var cpuUsage = GetCPUUsage();
if (cpuUsage > 0.8) // 80%
{
throw new OperationCancelledException();
}
}
}
var engine = new Engine(options =>
{
options.Constraint(new MyCPUConstraint());
});
当你重复使用引擎并想要使用取消令牌时,你必须在每次调用 Execute
之前重置令牌:
var engine = new Engine(options =>
{
options.CancellationToken(new CancellationToken(true));
});
var constraint = engine.Constraints.Find<CancellationConstraint>();
for (var i = 0; i < 10; i++)
{
using (var tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
constraint.Reset(tcs.Token);
engine.SetValue("a", 1);
engine.Execute("a++");
}
}
使用模块
你可以使用模块从多个脚本文件中 import
和 export
变量:
var engine = new Engine(options =>
{
options.EnableModules(@"C:\Scripts");
})
var ns = engine.Modules.Import("./my-module.js");
var value = ns.Get("value").AsString();
默认情况下,模块解析算法将被限制在 EnableModules
中指定的基本路径内,并且没有包支持。但是你可以通过两种方式提供自己的包。
使用 JavaScript 源代码定义模块:
engine.Modules.Add("user", "export const name = 'John';");
var ns = engine.Modules.Import("user");
var name = ns.Get("name").AsString();
使用模块构建器定义模块,这允许你从 .NET 导出 CLR 类和值:
// 创建包含 MyClass 类和 version 变量的 'lib' 模块
engine.Modules.Add("lib", builder => builder
.ExportType<MyClass>()
.ExportValue("version", 15)
);
// 创建一个用户定义的模块并使用 'lib'
engine.Modules.Add("custom", @"
import { MyClass, version } from 'lib';
const x = new MyClass();
export const result as x.doSomething();
");
// 导入用户定义的模块;这将执行导入链
var ns = engine.Modules.Import("custom");
// 结果包含对模块的"实时"绑定
var id = ns.Get("result").AsInteger();
注意,如果你只使用通过 Engine.Modules.Add
创建的模块,则不需要 EnableModules
。
.NET 互操作性
- 从 JavaScript 操作 CLR 对象,包括:
- 单个值
- 对象
- 属性
- 方法
- 委托
- 匿名对象
- 将 JavaScript 值转换为 CLR 对象
- 原始值
- Object -> expando 对象(
IDictionary<string, object>
和 dynamic) - Array -> object[]
- Date -> DateTime
- number -> double
- string -> string
- boolean -> bool
- Regex -> RegExp
- Function -> Delegate
- 扩展方法
安全性
以下功能为你提供了一个安全的沙箱环境来运行用户脚本。
- 定义内存限制,以防止分配耗尽内存。
- 启用/禁用 BCL 的使用,以防止脚本调用 .NET 代码。
- 限制语句数量,以防止无限循环。
- 限制调用深度,以防止深度递归调用。
- 定义超时,以防止脚本运行时间过长。
分支和发布
- 推荐的分支是 main,任何 PR 都应该针对这个分支
- main 分支会自动构建并发布在 MyGet 上。将此源添加到你的 NuGet 源以使用它:https://www.myget.org/F/jint/api/v3/index.json
- main 分支偶尔会发布在 NuGet 上