自第二种编程语言发明以来,语言之间的互操作性一直是一个问题。解决方案范围从语言无关的对象模型(如 COM(组件对象模型)和 CORBA(公共对象请求代理体系结构))到旨在集成语言的 VM(虚拟机)(如 JVM(Java 虚拟机)和 CLR(公共语言运行时))不等。随着软件变得越来越复杂,硬件变得越来越异构,单一语言成为整个程序的正确工具的可能性比以往任何时候都低。随着现代编译器变得更加模块化,新一代有趣的解决方案具有潜力。
1961 年,英国公司 Stantec 发布了一款名为 ZEBRA 的计算机,这款计算机因多种原因而引人注目,其中最重要的是其基于数据流的指令集。ZEBRA 使用其原生指令集的完整形式进行编程非常困难,因此它还包含一个更传统的版本,称为 Simple Code。这种形式有一些限制,包括每个程序最多 150 条指令的限制。该手册非常有帮助地告知用户,这不是一个严重的限制,因为有人不可能编写出如此复杂的工作程序,以至于需要超过 150 条指令。
今天,这种说法似乎很荒谬。即使是 C 这种相对低级语言中的简单函数,一旦编译后也超过 150 条指令,而且大多数程序都远远不止一个函数。从编写汇编代码到使用更高级语言编写代码的转变极大地增加了可能实现的程序的复杂性,各种软件工程实践也是如此。
软件复杂性增加的趋势没有减弱的迹象,现代硬件带来了新的挑战。20 世纪 90 年代后期的程序员必须以低端的 PC 为目标,这些 PC 的抽象模型很像快速的 PDP-11。在高端,他们会遇到一个抽象模型,就像非常快的 PDP-11,可能带有两到四个(相同的)处理器。现在,移动电话开始出现,配备八个内核,具有相同的 ISA(指令集架构),但速度不同,还有一些针对不同工作负载(DSP、GPU)优化的流处理器以及其他专用内核。
传统上,高级语言代表类似于人类对问题领域的理解的类别,而低级语言代表类似于硬件的类别,这种划分不再适用。没有低级语言具有接近可编程数据流处理器、x86 CPU、大规模多线程 GPU 和 VLIW(超长指令字)DSP(数字信号处理器)的语义。想要从可用硬件中获得最后一点性能的程序员不再有一种可以用于所有可能目标的单一语言。
同样,在抽象频谱的另一端,领域特定语言越来越流行。高级语言通常以通用性换取高效表示算法子集的能力。更通用的高级语言(如 Java)牺牲了直接操作指针的能力,以换取为程序员提供更抽象的内存模型。SQL 等专用语言使某些类别的算法无法实现,但使其领域内的常见任务可以用几行代码表达。
您不能再期望用单一语言编写重要的应用程序。高级语言通常调用用低级语言编写的代码作为其标准库的一部分(例如,GUI 渲染),但添加调用可能很困难。
特别是,两种非 C 语言之间的接口通常很难构建。即使是相对简单的示例,例如 C++ 和 Java 之间的桥接,通常也不是自动处理的,并且需要 C 接口。Kaffe Native Interface4 确实提供了一种执行此操作的机制,但它没有被广泛采用并且存在局限性。
对于编译器编写者来说,语言之间接口的问题将在未来几年变得越来越重要。它提出了许多挑战,此处详细介绍。
面向对象的语言将代码和数据的某些概念绑定在一起。Alan Kay 在 Xerox PARC 工作期间帮助开发了面向对象编程,他将对象描述为“通过消息传递进行通信的简单计算机”。这个定义为不同的语言填充细节留下了很大的余地
• 语言中是否应该有工厂对象(类)作为一等构造?
• 如果有类,它们也是对象吗?
• 对象应该有零个(例如,Go)、一个(例如,Smalltalk、Java、JavaScript、Objective-C)还是多个(例如,C++、Self、Simula)超类或原型?
• 方法查找是否与静态类型系统(如果存在)相关联?
• 对象内包含的数据是静态布局还是动态布局?
• 是否可以在运行时修改方法查找?
多重继承问题是最常见的关注领域之一。单继承很方便,因为它简化了实现的许多方面。可以通过简单地附加字段来扩展对象;转换为超类型只需忽略结尾,转换为子类型只需进行检查——指针值保持不变。C++ 中的向下转型需要在运行时库函数中通过运行时类型信息复杂地搜索继承图。
孤立地看,两种类型的继承都可以实现,但是如果您想例如将 C++ 对象暴露给 Java 会发生什么?您或许可以遵循 .NET 或 Kaffe 方法,并且仅支持与 C++ 子集的直接互操作性(托管 C++ 或 C++/CLI),该子集仅支持将暴露在 Java 端屏障上的类的单继承。
这通常是一个很好的解决方案:定义一种语言的子集,该子集可以清晰地映射到另一种语言,但可以理解另一种语言的全部功能。这是 Pragmatic Smalltalk5 中采用的方法:允许 Objective-C++ 对象(可以具有 C++ 对象作为实例变量并调用其方法)直接暴露,就像它们是 Smalltalk 对象一样,共享相同的底层表示。
然而,这种方法仍然提供了认知障碍。如果您想直接使用 C++ 框架,例如来自 Pragmatic Smalltalk 或 .NET 的 LLVM,那么您将需要编写单继承类,这些类封装了库用于其大多数核心类型的多重继承类。
另一种可能的方法是避免暴露对象内的任何字段,而只是将每个 C++ 类暴露为接口。然而,这将使得不可能从桥接类继承,除非有特殊的编译器支持来理解某些接口带有实现。
虽然复杂,但这比在方法查找含义不同的语言之间进行接口更简单的系统。例如,Java 和 Smalltalk 具有几乎相同的对象和内存模型,但 Java 将方法调度的概念与类层次结构联系起来,而在 Smalltalk 中,如果两个对象实现了具有相同名称的方法,则可以互换使用。
这是 RedLine Smalltalk1 遇到的问题,它将 Smalltalk 编译为在 JVM 上运行。它实现 Smalltalk 方法调度的机制包括为每个方法生成 Java 接口,然后在调度之前将接收器强制转换为相关的接口类型。向 Java 类发送消息需要额外的信息,因为现有的 Java 类不实现此功能;因此,RedLine Smalltalk 必须退回到使用 Java 的反射 API。
Smalltalk(和 Objective-C)的方法查找更复杂,因为存在许多在其他语言中缺失或受限的二次机会调度机制。在将 Objective-C 编译为 JavaScript 时,与其使用 JavaScript 方法调用,不如将每个 Objective-C 消息发送包装在一个小函数中,该函数首先检查该方法是否实际存在,如果不存在,则调用一些查找代码。
这在 JavaScript 中相对简单,因为它以一种方便的方式处理可变参数函数:如果使用比预期更多的参数调用函数或方法,那么它会收到其余参数作为它可以期望的数组。Go 做了类似的事情。类似 C 的语言只是将它们放在堆栈上,并期望程序员在没有错误检查的情况下进行写入。
内存模型中明显的二分法是在自动和手动释放之间。稍微更重要的考虑因素是确定性销毁和非确定性销毁之间的差异。
在许多情况下,可以使用 Boehm-Demers-Weiser 垃圾收集器3 运行 C 而不会出现问题(除非您耗尽内存并且有很多看起来像指针的整数)。对于 C++ 来说,这样做要困难得多,因为对象释放是一个可观察的事件。考虑以下代码
{
LockHolder l(mutex);
/* 执行需要锁定互斥锁的操作 */
}
的LockHolder类定义了一个非常简单的对象;互斥锁传递到对象中,然后在其构造函数中锁定互斥锁,并在析构函数中解锁互斥锁。现在,想象一下在完全垃圾收集的环境中运行相同的代码——析构函数运行的时间未定义。
这个例子相对容易理解。垃圾收集的 C++ 实现需要在此时运行析构函数,但不释放对象。这种习惯用法在从一开始就设计为支持垃圾收集的语言中不可用。混合使用它们的根本问题不是确定谁负责释放内存;而是为一种模型编写的代码期望确定性操作,而为另一种模型编写的代码则不期望确定性操作。
有两种微不足道的方法可以实现 C++ 的垃圾收集:第一种是使delete运算符调用析构函数但不回收底层存储;另一种是使deletea 空操作,并在检测到对象不可达时调用析构函数。
仅调用delete的析构函数在两种情况下都是相同的:它们实际上是空操作。释放其他资源的析构函数是不同的。在第一种情况下,它们确定性地运行,但如果程序员不显式删除相关对象,则将无法运行。在第二种情况下,保证它们最终会运行,但不一定在底层资源耗尽之前。
此外,在许多语言中,相当常见的习惯用法是自拥有对象,该对象等待某个事件或执行长时间运行的任务,然后触发回调。回调的接收者负责清理通知程序。虽然它是活动的,但它与对象图的其余部分断开连接,因此看起来是垃圾。必须明确告知收集器它不是垃圾。这与没有自动垃圾收集的语言中的模式相反,在这些语言中,除非系统另有告知,否则假定对象是活动的。(Hans Boehm 在 1996 年的一篇论文2 中更详细地讨论了其中一些问题。)
所有这些问题都存在于 Apple 不幸的(并且,值得庆幸的是,不再支持的)尝试中,即向 Objective-C 添加垃圾收集。许多 Objective-C 代码依赖于在-dealloc方法中运行代码。另一个问题与互操作性问题密切相关。该实现同时支持跟踪内存和非跟踪内存,但未在类型系统中公开此信息。考虑以下代码片段
void allocateSomeObjects (id * buffer, int count)
{
for (int i=0 ; i<count ; i++)
{
buffer [i] = [SomeClass new];
}
}
在垃圾收集模式下,不可能判断此代码是否正确。它是否正确取决于调用者。如果调用者传递使用NSAllocateCollectable()分配的缓冲区,其中NSScannedOption作为第二个参数,或者使用在启用垃圾收集支持的编译单元中的堆栈或全局变量上分配的缓冲区,则对象将持续(至少)与缓冲区一样长的时间。如果调用者传递使用malloc()分配的缓冲区,或者作为 C 或 C++ 编译单元中的全局变量,则对象将(可能)在缓冲区之前被释放。这句话中的可能使这成为一个更大的问题:由于它是非确定性的,因此很难调试。
Objective-C 的 ARC(自动引用计数)扩展不提供完整的垃圾收集(它们仍然允许垃圾循环泄漏),但它们确实扩展了类型系统以定义此类缓冲区的所有权类型。将对象指针复制到 C 需要插入显式强制转换,其中包含所有权转移。
引用计数还解决了非循环数据的确定性问题。此外,它还提供了一种与手动内存管理互操作的有趣方式:通过使free()递减引用计数。循环(或可能循环)数据结构需要添加循环检测器。David F. Bacon 在 IBM 的团队已经为循环检测器8 制作了许多设计,这些设计允许引用计数成为完整的垃圾收集机制,只要指针可以被准确识别。
不幸的是,循环检测涉及从可能循环的对象遍历整个对象图。可以采取一些简单的步骤来减少这种成本。显而易见的方法是推迟它。只有当对象的引用计数递减但未释放时,它才可能是循环的一部分。如果稍后递增,则它不是垃圾循环的一部分(它可能仍然是循环的一部分,但您还不关心)。如果稍后释放,则它是非循环的。
您推迟循环检测的时间越长,您获得的非确定性就越多,但循环检测器必须做的工作就越少。
如今,大多数人认为异常是 C++ 推广的意义上的异常:与setjmp()和longjmp()在 C 中大致等效的东西,尽管可能具有不同的机制。
已经提出了许多其他异常机制。在 Smalltalk-80 中,异常完全在库中实现。该语言提供的唯一原语是,当您显式从闭包返回时,您从声明闭包的作用域返回。如果您向下传递闭包堆栈,则返回将隐式展开堆栈。
当 Smalltalk 异常发生时,它会在堆栈顶部调用处理程序块。然后,它可以返回,强制堆栈展开,或者它可以进行一些清理。堆栈本身是激活记录(即对象)的列表,因此可能会执行更复杂的操作。Common Lisp 也提供了丰富的异常集,包括那些支持在之后立即恢复或重新启动的异常。
即使在具有相似异常模型的语言中,异常互操作性也很困难。例如,C++ 和 Objective-C 都具有类似的异常概念,但是当 C++ catch 块期望捕获void*时,当它遇到 Objective-C 对象指针时应该做什么?在 GNUstep Objective-C 运行时6 中,我们选择不捕获它,因为我们决定不模拟 Apple 的分段错误行为。最近版本的 OS X 采用了这种行为,但该决定在某种程度上是任意的。
即使您从 C++ 中捕获了对象指针,也并不意味着您可以对其执行任何操作。当捕获到它时,您已经丢失了所有类型信息,并且无法确定它是否是 Objective-C 对象。
当您开始考虑性能时,会出现更微妙的问题。早期版本的 VMKit7(在 LLVM 之上实现 Java 和 CLR VM)使用了为 C++ 设计的零成本异常模型。这是零成本,因为进入 try 块不花费任何成本。但是,在抛出异常时,您必须解析一些描述如何展开堆栈的表,然后为每个堆栈帧调用一个个性函数,以确定是否(以及在何处)应捕获异常。
这种机制非常适用于 C++,其中异常很少见,但 Java 使用异常来报告许多相当常见的错误情况。在基准测试中,展开器的性能是一个限制因素。为了避免这种情况,为可能抛出异常的方法修改了调用约定。这些函数将异常作为第二个返回值返回(通常在不同的寄存器中),并且每个调用都只需检查该寄存器是否包含 0,如果不是,则跳转到异常处理块。
当您控制每个调用者的代码生成器时,这很好,但在跨语言场景中并非如此。您可以通过向 C 添加另一个调用约定来解决此问题,该约定反映此行为或提供类似于 Go 中常用的多返回值机制来返回错误条件,但这将要求每个 C 调用者都意识到外语语义。
当您开始将函数式语言包含在您希望与之互操作的集合中时,可变性的概念变得重要。Haskell 等语言没有可变类型。就地修改数据结构是编译器可能作为优化执行的操作,但它不是语言中公开的内容。
这是 F# 遇到的问题,F# 作为 OCaml 的方言出售,可以与其他 .NET 语言集成,使用用 C# 编写的类等等。C# 已经具有可变类型和不可变类型的概念。这是一个非常强大的抽象,但是不可变类只是一个不公开任何非只读字段的类,而只读字段可能包含对对象的引用,这些对象(通过任意引用链)引用可变对象,这些对象的状态可能会在函数式代码下被更改。在其他语言(如 C++ 或 Objective-C)中,可变性通常在类系统中实现,方法是定义一些不可变的类,但没有语言支持,也没有简单的方法来确定对象是否可变。
C 和 C++ 在语言提供的类型系统中具有非常不同的可变性概念:对对象的特定引用可能会或可能不会修改它,但这并不意味着对象本身不会改变。这与深层复制问题相结合,使得函数式语言和面向对象语言的接口成为一个难题。
Monad 为接口提供了一些诱人的可能性。Monad 是计算步骤的有序序列。在面向对象的世界中,这是一系列消息发送或方法调用。具有 C++ const 概念(即不修改对象状态)的方法可以在 monad 之外调用,因此适合推测执行和回溯,而其他方法应在 monad 定义的严格序列中调用。
可变性和并行性密切相关。编写可维护、可扩展的并行代码的基本规则是,任何对象都不能既是可变的又是别名的。这在纯函数式语言中很容易强制执行:根本没有对象是可变的。Erlang 在可变性方面做出了一项让步,即进程字典——一个只能从当前 Erlang 进程访问的可变字典,因此永远不会被共享。
具有不同共享概念的语言之间的接口提出了一些独特的问题。这对于旨在面向大规模并行系统或 GPU 的语言来说很有趣,在这些语言中,语言的模型与底层硬件紧密相关。
这是在尝试提取 C/C++/Fortran 程序的某些部分以转换为 OpenCL 时遇到的问题。源语言通常将就地修改作为实现算法的最快方法,而 OpenCL 鼓励一种模型,其中处理源缓冲区以生成输出缓冲区。这很重要,因为每个内核都在许多输入上并行运行;因此,为了获得最大吞吐量,它们应该是独立的。
但是,在 C++ 中,确保两个指针不别名并非易事。restrict关键字的存在是为了允许程序员提供此注释,但在一般情况下,编译器不可能检查它是否被正确使用。
高效的互操作性对于异构多核系统非常重要。在传统的单核或 SMP(对称多处理)计算机上,在接近问题领域的高级语言和接近架构的低级语言之间存在一维频谱。在异构系统上,没有一种语言接近底层架构,正如在 GPU 上运行任意 C/C++ 和 Fortran 代码的困难所表明的那样。
当前的接口(例如,OpenCL)远非理想。程序员必须编写 C 代码来管理设备上下文的创建以及与设备之间的数据移动,然后用 OpenCL C 编写内核。用另一种语言表达在设备上运行的部分是有用的,但是当简单操作的大部分代码都与两个处理元件之间的边界而不是任何一侧完成的工作相关时,那么就有些问题了。
如何向程序员公开具有非常不同抽象机器模型的多个处理单元是一个有趣的研究问题。很难提供一种能够有效捕获语义的单一语言。因此,这个问题变成了专用语言之间的互操作性问题。这是一个有趣的转变,因为传统上位于频谱高端的领域特定语言现在在作为低级语言方面发挥着越来越重要的作用。
虚拟机通常被吹捧为解决语言互操作性问题的一种方法。当 Java 推出时,其中一个承诺是您很快就能够编译所有遗留的 C 或 C++ 代码,并在 JVM 中与 Java 一起运行,从而提供清晰的迁移路径。今天,Ohloh.net(跟踪公共开源存储库中可用代码行数的网站)报告有 40 亿行 C 代码,以及大约 15 亿行 C++ 和 Java 代码。虽然 Scala 等其他语言(Ohloh.net 跟踪的代码行数接近 600 万行)在 JVM 中运行,但遗留的低级语言却没有。
更糟糕的是,从 Java 调用本机代码非常麻烦(在认知和运行时开销方面),以至于开发人员最终用 C++ 编写应用程序,而不是面对从 Java 调用 C++ 库。Microsoft 的 CLR 做得稍好一些,允许用 C++ 子集编写的代码运行;它使调用本机库更容易,但仍然提供了一堵墙。
对于 Smalltalk 等没有大公司支持的语言来说,这种方法是一场灾难。Smalltalk VM 提供了一些 CLR 和 JVM 都没有的优势,例如持久性模型和反射式开发环境,但它也通过将世界划分为盒子内部和盒子外部的事物,形成了非常大的 PLIB(编程语言互操作性障碍)。
一旦您有两个或多个 VM,情况会变得更加复杂,现在您遇到了源语言互操作性问题以及两个 VM 之间(非常相似的)互操作性问题,这两个 VM 通常是非常低级的编程语言。
多年前,当时最大的互操作性问题是 C 和 Pascal——两种具有几乎相同抽象机器模型的语言。问题是 Pascal 编译器从左到右将其参数压入堆栈(因为这需要的临时变量更少),而 C 编译器从右到左压入堆栈(以确保第一个参数位于可变参数函数堆栈的顶部)。
这个互操作性问题在很大程度上通过简单地将调用约定定义为平台 ABI(应用程序二进制接口)的一部分来解决。不需要虚拟机或中间目标,也不需要任何源到源的翻译。虚拟机的等效项由 ABI 和目标机器的 ISA 定义。
Objective-C 提供了另一个有用的案例研究。Objective-C 中的方法使用 C 调用约定,首先传递两个隐藏参数(对象和选择器,它是方法名称的抽象形式)。语言中所有不能简单地映射到目标 ABI 或 ISA 的部分都被分解为库调用。方法调用被实现为对objc_msgSend()函数的调用,该函数被实现为一个简短的汇编例程。所有内省都通过调用运行时库的机制工作。
我们使用 GNUstep 的 Objective-C 运行时在 LanguageKit 中实现 Smalltalk 和 JavaScript 方言的前端。这使用了 LLVM,但这仅仅是因为拥有低级中间表示允许在编译器之间重用优化:互操作性发生在本机代码中。此运行时还支持 Apple 定义的块 ABI;因此,闭包可以在 Smalltalk 和 C 代码之间传递。
Boehm GC(垃圾收集器)和 Apple AutoZone 都旨在以库形式提供垃圾收集,但具有不同的要求。并发压缩收集器可以作为库公开吗?当对象传递给低级代码时,是否可以将对象单独标记为不可移动?是否可以在 ABI 或库中强制执行可变性和并发保证?这些都是开放性问题,编译器设计的成熟库的可用性使它们成为有趣的研究问题。
也许更有趣的问题是,其中有多少可以沉入硬件中。在 CTSRD(崩溃安全可信系统研发)中,这是 SRI International 和剑桥大学计算机实验室之间的联合项目,研究人员一直在尝试将细粒度的内存保护放入硬件中,他们希望这将为表达某些语言内存模型提供更有效的方法。这是一个开始,但在硅中为高级语言提供更丰富的功能集还有很大的潜力,这在 20 世纪 80 年代被避免了,因为当时的晶体管稀缺且资源昂贵。现在晶体管充足但电力稀缺,因此 CPU 设计中的权衡非常不同。
过去 30 年,业界一直致力于构建针对 C 等语言优化的 CPU,因为需要快速代码的人使用 C 语言(因为处理器设计者针对 C 语言进行了优化,因为...)。或许现在是时候开始探索为其他语言中的常见操作提供更好的内置支持了。RISC 项目的诞生源于观察从编译 C 代码生成的原始编译器指令。如果我们从观察原生 JavaScript 或 Haskell 编译器会发出什么指令开始,最终会得到什么?
1. Allen, S. 2011. RedLine Smalltalk。在国际 Smalltalk 会议上发表。
2. Boehm, H.-J. 1996. 简单的垃圾回收器安全性。《 SIGPLAN Notices》31(5):89-98。
3. Boehm, H.-J., Weiser, M. 1988. 非合作环境中的垃圾回收。《软件实践与经验》18(9): 807-820。
4. Bothner, P., Tromey, T. 2001. Java/C++ 集成;http://per.bothner.com/papers/UsenixJVM01/CNI01.pdf。 http://gcc.gnu.org/java/papers/native++.html
5. Chisnall, D. 2012. C 世界中的 Smalltalk。在《国际 Smalltalk 技术研讨会论文集》中:4:1-4:12。
6. Chisnall, D. 2012. 一种新的 Objective-C 运行时:从研究到生产。《》。 https://queue.org.cn/detail.cfm?id=2331170
7. Geoffray, N., Thomas, G., Lawall, J., Muller, G., Folliot, B. 2010. VMKit:托管运行时环境的基底。《 SIGPLAN Notices》45(7): 51-62。
8. Paz, H., Bacon, D. F., Kolodner, E. K., Petrank, E., Rajan, V. T. 2005. 一种高效的在线循环回收方法。在《第 14 届国际编译器构造会议论文集》中:156-171。柏林,海德堡:施普林格出版社。
这项工作的部分内容由 DARPA(国防高级研究计划局)和 AFRL(空军研究实验室)在 FA8750-10-C-0237 合同下赞助。本报告中包含的观点、意见和/或发现是作者的观点,不应被解释为代表 DARPA 或国防部的明示或暗示的官方观点或政策。
喜欢还是讨厌?请告诉我们
David Chisnall 是剑桥大学的研究员,他在那里从事编程语言设计和实现方面的工作。在完成博士学位并来到剑桥大学之间,他花了几年时间担任顾问,在此期间他还撰写了关于 Xen 以及 Objective-C 和 Go 编程语言的书籍,以及大量文章。他还为 LLVM、Clang、FreeBSD、GNUstep 和 Étoilé 开源项目做出了贡献,并且他跳阿根廷探戈。
© 2013 1542-7730/13/1000 $10.00
最初发表于 Queue vol. 11, no. 10—
在 数字图书馆 中评论这篇文章
Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息方面需要权衡:这可能会使编译速度变慢。链接时优化等技术可以为您提供两全其美的优势。编译器中的优化持续改进,即将到来的间接调用和虚拟函数分派方面的改进可能很快会带来更快的多态性。
Ulan Degenbaev, Michael Lippautz, Hannes Payer - 垃圾回收作为合资企业
跨组件跟踪是解决跨组件边界的引用循环问题的一种方法。只要组件可以形成任意对象图,并在 API 边界上具有重要的所有权,就会出现此问题。CCT 的增量版本已在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。
David Chisnall - C 语言不是一种低级语言
在最近的 Meltdown 和 Spectre 漏洞之后,值得花一些时间来研究根本原因。这两个漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能的添加,是为了让 C 程序员继续相信他们正在用低级语言编程,而这种情况已经几十年没有发生了。
Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知存在漏洞。了解问题的范围,以及包含库的许多意外方式,只是改善情况的第一步。这里的目标是,本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。