Cakelisp:游戏编程语言

2020-12-21 11:19:09

自2020年8月以来,我一直在研究一种新的编程语言。该语言托管在Github上,并反映在我的网站上。

如果您想首先看到一个工作示例,VocalGame.cake是一个简单的音频循环程序,带有Ogre 3D图形和SDL,用于开窗,输入和音频。该演示支持代码热重载,并且不需要外部构建系统,仅需要Cakelisp。您还可以检查Macros.cake,该示例演示了一些编译时代码的用例。

我认为显示非平凡的例子会更有趣。这也证明Cakelisp可以正常工作。

Cakelisp首先是为我构建的,但它应该吸引那些知道自己在做什么并且想尝试一种更强大的语言的程序员。

如果该列表中的任何内容对您没有意义,或者您认为您已经在用X语言获取它们,那么Cakelisp不适合您,就可以了!我们有不同的领域和不同的问题,因此使用不同的语言和方法是有意义的。

尽管许多语言都具有这些功能,但是很少有这三种语言的结合。 Lisp具有非常强大的代码生成功能,但会严重影响性能。 C的性能非常好,但是可能需要非常重复的代码编写才能完成简单的代码生成器可以处理的任务。 Rust速度很快(嗯,除了编译,这对于提高迭代开发的生产率非常重要),但是Rust不信任程序员。

我的目标是要拥有我的蛋糕,也要吃它,这意味着所有这三个功能都集成在一个包装中。重要的是,Cakelisp(无大想法)中没有一个主导原则。我发现,诸如减少对头文件的需要,不再与外部构建系统打交道或能够运行诸如脚本之类的Cakelisp文件这样的小事,最终将它们组合在一个软件包中时,会产生很大的不同。

详细了解目标很有用,这样您就可以了解我的决定。

这意味着没有垃圾回收,没有类型装箱/拆箱等。在您与计算机实际所做的工作之间(除了您创建的对象之外)的抽象较少。惯用该语言应会产生与C相当的性能(在大多数情况下,它应该是相同的,因为它只是C上的一薄层)。

尽管诸如Rust之类的语言在安全性和稳定性方面提供了好处,但在生产力方面却使程序员付出了代价。如果您的代码对安全性至关重要(操作系统,航空航天,汽车等),那么高度重视安全性是有意义的,但是当安全性不那么重要时(例如在游戏中),它的价值就不那么重要了。 。

在一个理想的世界中,所有程序都将与太空飞行软件一样强大,但是实际上,大多数程序都不需要这种鲁棒性。重要的是要认识到,安全重点只是做事的一种方式,而不是“真相”或其他任何方式。

在我看来,大多数语言为程序员提供了很少的机会来使代码的实际编写自动化。这种能力也与信任程序员有关,因为变得疯狂,代码可能变得难以理解。

我工作的公司具有我认为是针对公司用例构建的最新代码生成器:多平台MMO。它非常有效地用于序列化,RPC,自动命令,监视,自动文档等等。

为了使游戏中使用代码生成更加可信,Unreal和Naughty Dog也依靠代码生成。

我想大大减少浪费在C ++项目设置和"代码物流"上的时间。这包括设置构建系统,创建头文件,添加和管理新的C / C ++ 3rd party库以及其他类似功能。

每种语言都有局限性。缺乏直接,功能强大的代码生成是我使用C ++的主要障碍。

例如,使用C ++模板元编程自动创建功能和结构绑定非常复杂。这是游戏开发中两个非常有用的工具:命令,脚本语言和RPC的函数绑定;用于序列化或游戏监视器的结构绑定。

我还需要诸如热重载之类的功能(能够在不重新启动程序的情况下加载新版本的代码/不会丢失运行时状态)。由于修改了代码,Cakelisp使得完全可以在" user-space"中实现热重装成为可能。

我的意思是说,我在关心的指标上几乎没有损失,其中包括构建时间,运行时性能,总体复杂性以及其他各种因素。我在LanguageTests实验中研究了几种语言,发现它们都有我无法接受的主要缺点。

当我启动Cakelisp时,我没有意识到释放它的感觉。突然之间,我必须决定什么对我有意义,而不是对以前的语言设计师有意义。

我认为C类型声明比我的显式类型声明更难解析。您需要从名称倒退以正确解释类型。括号的确增加了更多的键入内容,但是它们更加清晰,可机器解析,并且可以自然地读取(例如,从左向右读取指向常量字符的指针,与C和#34;常量字符指针"在我看来似乎更糟)。

我的表单也将数组作为类型的一部分来处理:(var my-array([] 5 int))而不是int myArray [5] ;,这是另一种更一致,易读和可解析的方法。

我选择交换名称和类型的顺序,因为它更加强调名称。一个写得很好的程序将在名称中传达比在类型中更有用的信息,因此对读者来说,让我知道它是最有意义的。

我还发现,拥有一个可以按照我所希望的方式对其代码进行预处理的可执行文件,这为大量令人敬畏的功能打开了大门:

编译时代码执行。 " Macros"和" Generators"是与您的其余代码内联定义的,使它们看起来像是您代码的自然组成部分。内联定义它们可以添加一次性宏,而将此类内容添加到外部代码生成器将很快变得难以维护

构建优化。我发现的最新想法是自动为大批的第三方标头创建预编译的标头。这将是一项复杂的任务,需要集成到您使用的任何构建系统中,而Cakelisp可以将其内置

其他数据处理。编译时代码执行意味着您可以执行诸如准备资产,下载第三方代码,运行测试等操作,而无需设置所有其他工具

我受到Naughty Dog使用游戏导向装配Lisp,GOOL和Racket / Scheme(在其现代头衔上)的启发。我还从乔纳森·布洛(Jonathan Blow)关于Jai的谈话中获得了一些想法。

我是游戏行业的软件工程师。自2015年7月以来,我一直在一家制作跨平台MMO的工作室工作。该公司有一个用C(带有一些C ++)编写的自定义引擎。

现在我的目标已经明确,我将向您展示如何实现这些目标。

类型。 Cakelisp是强类型和显类型的。我更喜欢使用显式类型的代码,因为我可以更好地想象计算机的实际运行情况,以及每个变量有什么可能性

名称类型顺序。我在上一节中谈到了这一点。我想强调一个变量的名称以传达含义,尤其是当您可能有许多相同类型的变量时

明确的回报。我发现我更喜欢将返回点明确化的代码。 Lisp将隐式返回上一次评估的结果

Lisp-y样式。括号,加上关键字,例如,除非,defun,var,at和incr。只有当我对更好的表示法没有强烈的意见时,我才与Lisp匹配。我不尝试创建与现有Lisps兼容的产品

C类型和函数调用。 Cakelisp具有无缝的C互操作,这意味着Cakelisp的标准库。是C的标准库。无需编写绑定即可使用C类型或进行函数调用

您应该将Cakelisp更视为S表达式中的&C;"而不是“具有C性能的Lisp”。如果您知道C,就可以轻松过渡到Cakelisp。如果您只了解Lisp,那么您将度过一段艰难的时光。

当我着手制作Cakelisp时,出于以下几个原因,我决定使用S-expressions语法:

可解析性。 S表达式将创建语法树的负担转移给了程序员。这的确为人类带来了更多的工作,但是我很重视它的极其明确的本质。它还促进了更简单的标记化,特定于域的语言实现以及外部工具支持

一致性。 Cakelisp中只有四种类型的标记:打开和关闭括号,符号和字符串。诚然,一致性是有限的,因此不幸的是,诸如path(myThing-> member.member)之类的东西变得更加冗长。但是,此限制使Cakelisp代码可解析,并具有让我欣赏的优雅感觉

我不相信有一个规则可以全部统治它们,尤其是在遇到使用S-exprs的缺点之后。不过,我仍然对这个决定感到满意,它确实为Cakelisp提供了一种新颖且与众不同的特征,与正在制作的许多C风格语言不同。

我选择通过使其成为编译器来实现Cakelisp,这意味着它不输出机器代码。它输出C ++(尽管我也计划支持纯C输出)。

大大加快了实施速度。如果我实现了一个可以生成机器代码的正确的编译器,那么距离可用的实现还差很多年。我没有时间也没有兴趣专注于该级别

良好的平台支持。 Cakelisp没有运行时,这意味着它生成的代码可以在C ++可以运行的任何地方运行,而该死的地方却无处不在。我必须输出到LLVM或类似的东西才能获得这种跨平台支持,这比我愿意花费的时间要多得多。

退出机会。如果由于某种原因Cakelisp崩溃并烧毁(或更可能是兴趣减弱),我可以最后一次输出C ++,并以有用的形式保存该有价值的代码,以供将来的项目使用。 C特别是增加代码寿命的一种有用形式,因为值得使用的每种语言都支持调用C代码

与C / C ++的无缝交互。 Cakelisp的一个重要目标是永远不需要繁琐的绑定编写(并避免自动生成错误的绑定)。为了支持这一点,Cakelisp对类型和功能的了解非常有限。因此,肯定存在一些较难实现的功能(例如,更高级的类型系统或内存类型注释),但是自然而完整地访问大量现有C / C ++代码对我来说更有价值。尤其是因为游戏严重依赖用C / C ++编写的库,并且在可预见的将来(包括控制台,中间件,诸如OpenGL / DirectX之类的API和直接的操作系统交互)将是可取的。

个人爱好。我对语言的高级感觉,界面,而不是汇编生成的细腻细节更感兴趣。输出C使我能够保持我满意的水平而不会损失太多性能(尽管C / C ++编译器的编译时间比我想要的要长得多,尤其是在看到Jai的速度之后)

有多种影响最终输出代码的方法,以及执行任意代码执行的机会。

Cakelisp自动确定编译时代码的依赖关系,并懒惰地构建其定义。请注意,Cakelisp没有解释器-所有代码在执行之前都会被编译成机器代码(这无疑会使第一代构建的时间比被解释的要长)。

当我说宏时,我指的是Lisp风格的宏,而不是C / C ++预处理器宏。不同之处在于Lisp风格的宏是可以执行任意计算的实际函数,而预处理器宏实质上是只能插入文本的模板系统。

宏将Cakeakep令牌数组作为输入,并可以输出任意令牌来代替宏的调用。

宏有助于创建特定于域的语言,其中语法的整体由程序员在该语言中定义。它们还可以用于简单替换,例如C预处理器宏,并可以选择添加范围或变量参数的验证。

虽然我还没有实现C或C ++的所有功能,但是生成器允许程序员访问这些语言的更多功能,而无需修改Cakelisp本身。

发电机构成了Cakelisp的基础。通过选项--list-built-ins,用户可以查看所有内置的生成器。至关重要的是,生成器去生成器有助于添加新的生成器。

处于编译和构建各个阶段的钩子使程序员可以极大地影响Cakelisp的行为。挂钩是函数列表,因此可以添加多个挂钩。

例如,可以将构建前挂钩设置为检查第三方库并在必要时构建它们。生成后挂钩可以复制或生成可执行文件运行或执行系统安装步骤所需的配置文件。

我还计划提供一种完全覆盖构建阶段的方法,如果您需要输出多个不同的可执行文件,库等,这将很有用。如果集成了该钩子,则可以使用此钩子更轻松地为外部构建系统生成配置文件很重要

代码生成创建代码;修改会更改现有代码。这为独特的功能打开了大门,而仅靠一代人的工作就不可能做到这一点。有关如何使用它的示例,请参见此Jai演示。

使用正确的钩子,可以执行任意代码修改。到目前为止,我已经两次使用此功能,以达到良好效果:

AutoTest.cake将搜索前缀为test的函数-并从main()函数中调用它们。这个68行的程序提供了一种对所有模块进行高级测试的简单方法,并且入侵最少

热重载会自动将全局变量和模块局部变量转换为使用堆,然后在访问它们时自动插入取消引用。这允许程序员对功能进行更改并动态重新加载它们而不会丢失状态。此功能非常适合促进迭代的低延迟开发。将其添加到构建系统中,您将获得快速,简单和便捷的开发环境

我本来以为我会依靠第三方构建系统来组装Cakelisp项目。我发现,取消手动头文件并使用受Python启发的模块系统使之不可行。 Cakelisp现在具有内置的构建系统。这最终具有一些巨大的优势:

Cakelisp模块可以声明它们链接到的库,它们在哪里,甚至可以构建任意的C / C ++文件。这些声明均与其余模块代码一致。

这使得在项目中包括新的第三方模块变得微不足道,因为构建配置保留在该模块内。简单地例如(导入" SDL.cake"),然后构建系统将处理所有添加的编译和链接标志,以使其实现。

从多项目的角度来看,这可以防止复制构建标志的复杂性和繁琐的工作扩散到模块本身之外。您进行的项目越多,回报就越高。

构建系统与它所构建的语言紧密相关,这意味着您可以添加提示并内联构建配置。

例如,我计划制作一个选择加入的预编译头文件功能,该功能可以大大加快包含大量头文件的文件编译速度。只需将& precompile标记添加到include列表中,Cakelisp将把它们全部存储到单个标头中,并使该标头保持最新状态。

如果遇到棘手的构建配置问题,可以将同一调试器用于编译时,运行时和构建系统调试(任何C ++调试器)。

通过包含构建系统,用户界面变得更加简单。 Cakelisp具有--execute标志,该标志将构建给定的.cake文件,然后执行输出的二进制文件。这是一个简单的添加,它使运行C / C ++品质的程序与脚本一样容易,同时仍保持性能并保持最新。

我还决定命令行参数永远不要改变编译器本身的行为。它们只能更改详细程度,执行--execute之类的其他操作或简化构建过程(--ignore-cache)。此约束导致对成功构建项目至关重要的配置驻留在项目代码中。当所有选项均为“内置”时,外部构建系统和.sh脚本的使用会减少。

该决定还鼓励创建可组合的构建配置,其中使用不同的构建配置会涉及导入不同的.cake模块或模块集合。构建配置标签组合在一起,例如:

…将使用配置标签Debug-HotReloadable构建。进行新配置是一站式更改:

这使您可以轻松进行复杂的配置,或尝试A / B测试之类的操作,而不必管理所有新的构建构件。

足够复杂的项目需要完整的编程语言来构建。集成的构建系统使程序员能够进行构建所需的任何工作,而无需程序员学习仅用于构建配置的其他(通常是劣等的)语言。请注意,在编译/构建期间任意执行代码意味着,如果您愿意的话,您可以做的不仅仅是构建项目(例如,将工件推送到服务器,下载代码,发布推文等等)。

顽皮狗联合创始人安迪·加文(Andy Gavin)谈及在实现游戏对象状态机时C如何差劲,这就是他制作GOOL的原因之一。我想探索这样的想法-更有效地实施游戏和其他程序的新方法。

例如,我有一种直觉,即保留模式的UI与特定领域的语言结合使用时可能会具有像Dear ImGui一样的立即模式感觉(它很快就变得流行,尤其是在游戏开发中)。它不会与实际的UI实现分开,例如需要模型和绑定进行更新的UI系统。在编译时,自定义宏将发现需要创建哪些UI小部件并适当地生成代码。

面向数据的设计引人注目,但在实现过程中通常需要跳过障碍。我想探索实现对缓存友好的同时仍对程序员友好的事物的方法(例如,参见Jai的SoA与AoS)。

计算机越来越多地获得更多的内核,但是编写多线程应用程序却像以往一样困难。游戏编程特别具有挑战性,因为单个游戏行为可能涉及多少系统(动画,音频,AI,实体与实体的交互,空间查询,物理……)。与上述安迪·加文(Andy Gavin)文章中的GOOL相似,对多线程开发更人性化的界面将非常有必要。

当然,有一些新功能要实现,有新的教训要学习。 我不打算让Cakelisp成为"玩具语言"。 我构建了它,以替换以后的项目中对C ++的使用。 在收养方面,我不希望每个人都喜欢Cakelisp并大声宣扬它。 我有一个相对特定的用例,Cakelisp非常适合我。 我不相信这对其他很多人来说都是完美的。 我宣布它的主要希望是公开 ......