策划(2019年)

2020-09-07 10:56:22

上周日,我花了几个小时重新学习与LISP相关的语言,部分原因是我错过了编写Clojure,部分原因是我想做一件相对简单的事情:发出一组HTTPS请求,整理产生的JSON数据,然后发出最后一个POST请求。我想用一个HTTP库,在尽可能小的空间内,用一个静态的二进制文件,做到这一点。三个人中有两个不会是坏人,对吧?

当然,没有人希望在现实生活中得到三个中的三个,但事实证明,在现代编程语言中,即使是三个中的两个也是一个相当大的挑战。

这个特殊的兔子洞部分是因为我的k3和Azure API恶作剧,部分是因为我有一个非常类似的问题,我一直在处理Python和请求,但它需要在更受限的环境中运行。

考虑到许多人不知道在这个时代做HTTPS请求需要做些什么,这总是令人惊讶的--您可能会认为“获取这个URL并解析JSON输出”在任何当前的编程语言中都等同于“Hello World”,然而这通常是一项非常曲折的任务…。

我的大多数朋友都告诉我“为什么不直接使用NodeJS或者Go呢?”

好吧,暂时忽略静态的二进制需求,因为NodeJS仍然一团糟。只需Google for NodeJS http请求,观察这个特定的轮子被重新发明了多少次(或者更好的是,查看内置http库的更改历史记录)。如果我必须在NodeJS中完成这项工作,出于理智和可读性的考虑,我会使用类似Request的包装器,而不是立即吸收(双关语)大量确实应该内置在…中的依赖项。

不相信我吗?看看核心https版本,它需要一个显式的数据泵:

Const https=Required(';https';);https。GET(';https://endpoint';,(Resp)=>;{let Data=';';;resp.。On(';data';,(Chunk)=>;{data+=chunk;});分别。在(';结束';,()=>;{控制台。日志(JSON.。Parse(Data));});})。在(";错误";,(错误)=>;{控制台。错误(错误。消息);});

Const Request=Required(';Request';);Request(';https://endpoint';,Function(Error,Response,Body){IF(Error){Console.。错误(';错误:';,错误);}其他{控制台。日志(JSON.。Parse(Body));}}));

…。我甚至不打算讨论定制请求头和cookie处理(这是Request真正让我的事情变得更容易的地方),或者如何以同样的方式工作于缓冲区、流等。

因此,尽管它有很多优点,但像这样的简单代码相对较难编写,而且容易出错。

例如,下面是我针对Azure实例元数据API所做的7行Python请求移植工作的一部分(请注意,这在很大程度上仍未经过测试)。请注意,当您开始使用用于解组的结构注释解析嵌套的JSON文档时(为了获得额外的Brownie点),整个过程会变得非常麻烦:

Import(";coding/json";";io/ioutil";";log";";net/http";)type InstanceMetadata struct{Compute struct{ResourceGroupName string`json:";resource GroupName";`SubscriptionID string`json:";scriptionId";`}`json:"。客户端{}请求,错误:=http。新请求(";GET";,";http://169.254.169.254/metadata/instance";,NIL),如果错误!=Nil{log.。致命(错误)}请求。标题。添加(";元数据";,";true";)q:=req.。URL。Query()q.。添加(";API-Version";,";2018-10-01";)请求。URL。RawQuery=q.。Encode()resp,err:=client。DO(Req)if err!=nil{log.。致命的(错误);}分别推迟。身体。关闭()主体,错误:=ioutil。全部读取(分别为。正文)if err!=nil{log.。致命(Err);}元数据:=InstanceMetadata{}err=json。如果err!=nil{log.(正文,&;元数据),则取消编组(正文,&;元数据)。致命(错误)}返回元数据(&A)}。

它和铁锈一样残忍,我觉得很有趣,但很难与庞大的围棋生态系统相提并论。

所以不,这不是我自己项目想要的那种开发人员体验,即使它在过去几年中几乎接管了整个现代“系统”编程。

对于我的爱好,我正在寻找一种更简洁的语言(因此更有可能是动态的/解释的),这样我就可以轻松地读写,这会给这个过程带来一些乐趣。

我也很想念LISP(我是那些真正喜欢它的人之一,因为它是一种解除苦差事的智力解毒剂,即使我确实喜欢Python和Go)。

因此,我决定用我必须解决的那个特定问题作为借口,来检查一下计算宇宙中那个角落的最新技术。多年来,我一直在跟踪很多Scheme实现,包括一些向下编译为C或本机代码的实现,所以现在是时候看看它们有多有用了。

下面的所有测试都是在Ubuntu18.04.2上运行的,既有WSL,也有我用来测试独立二进制文件的独立机器(我有时也使用16.04机器),因为它通常是我构建的目标Linux。

茴香不是我第一个尝试的东西,但它是第一个几乎击中目标的东西。这是一种编译为Lua的LISP,并且它以一种相当优雅的方式实现了这一点,将以下代码转换为:

(LOCAL Http(Required";ssl.https";))(Local Ltn12(Required";ltn12";))(Local Inspect(Required";Inspect";))(fn Request[url](local resp{})(let[(Res Code Headers)(http.request{";url";url";method";";get";";Sink";(ltn12.sink.table Resp)";Header";{";User-Agent";";";Lua";}})](打印(检查响应)(Request";https://endpoint";)。

本地http=要求(";ssl.https";)本地ltn12=要求(";ltn12";)本地检查=要求(";检查";)本地函数请求(Url)本地resp={}执行本地资源、代码、标头=http。请求({Headers={[";User-Agent";]=";Lua";},Method=";Get";,Sink=ltn12.。水槽。Table(Resp),url=url})返回打印(检查(响应))结束返回请求(";https://endpoint";)。

考虑到ltn12是您在Lua中很少能避免的事情,并且如果您忽略缺少人工创建的空格,那么生成的代码几乎是惯用的,考虑到这一点,结果是相当不错的。

上述代码不执行任何JSON解析,但是对于Lua也有一个请求岩石,它引入了更多的依赖项(XML和cjson),并使所有内容更具可读性:

茴香是一种简洁而美丽的东西,我可以(从理论上)使用像luapak这样的工具将所有东西编译成大约1MB的可执行文件(这是Lua运行时加上依赖项的粗略内存占用空间)。

但我不能设法让卢塞克配合,否则我很可能会停在这里。

不过,这非常诱人,因为使用Lua还可以让它在ESP8266/ESP32上运行,这对于我脑海中的另一种用途来说将是完美的。

我的下一站是Chicken Scheme,它是我的一个老相识,它编译成C语言,并且相当优雅地完成了我需要的一切--在Chicke4上,最简单的HTTPS和JSON解码调用如下所示:

上面的代码编译成只有35112字节的动态可执行文件,显然依赖于许多动态链接库。但不仅仅是它通过LDD报道的那些:

%ldd请求linux-vdso.so.1(0x00007fffc34bf000)libchicken.so.8=>;/usr/lib/libchicken.so.8(0x00007f0f8d830000)libc.so.6=>;/lib/x86_64-linux-gnu/libc.so.6(0x00007f0f8d430000)libmm.。/lib/x86_64-linux-gnu/libdl.so.2(0x00007f0f8ce80000)/lib64/ld-linux-x86-64.so.2(0x00007f0f8e600000)。

让我印象深刻的是,这既不包括OpenSSL,也不包括我为了构建它而必须安装的扩展:

使用strace运行可执行文件,并过滤输出以捕获所有openat调用,结果显示它动态加载了大约17MB的库,因此这将是它的最大内存使用量。

至于构建可重新分发的二进制文件,尝试使用-static和OpenSSL扩展作为依赖项进行构建在Chicke4中不起作用(这是我手边有的),但今天我在Chicke5中做了一些尝试-我设法链接了一些库,但不是我正在使用的所有扩展。

就方案而言,Sracket是非常受欢迎的选择(Ubuntu 18.04附带6.x包),所以我快速尝试了一下:

而且,令人惊讶的是,它是唯一一个给我提供了一套几乎没有麻烦的可重新分发的文件。

上面的代码(使用Raco exe)构建为一个6MB的文件,该文件仍然需要Racket才能运行,并且可以(使用Raco分发)打包到一个大约10MB的包中:

Dist├──[4.0K]bin│和└──[6.0M]Request└──[4.0K]lib└──[4.0K]Plt├──[3.9M]Racket3M-6.11└──[4.0K]Request├──[4.0K]收集└──[4.0K]EXTS└──[4.0K]ERT└──[4.0K]R0└──[1016]dh4096.pem。

更重要的是,当我将文件复制到“空白”机器并运行可执行文件时,它才起作用,并且是到目前为止唯一这样做的解决方案(事实上,两个版本都这样做了,只是最终的二进制大小略有不同)。

接下来是Chez Scheme,它正在成为球拍7.x版本的基础,但是我几乎一片空白--因为Chez虽然也附带了Ubuntu,但目前缺乏一套全面的库(尽管Thunderchez和Scheme-lib有大量的资料可以通过…)。。

几天后,我决定看一看Gerbil,它是Gambit Scheme的一个很好的包装器,碰巧是面向系统编程的。

沙土鼠包装Gambit的方式与球拍包装Chez的方式大致相同,但在其库中有一个更实用、更低级的转变。

就我对二进制文件的需求而言,构建一个与libssl和其他系统库有显式链接的二进制文件相对容易:

%ldd请求linux-vdso.so.1(0x00007ffffbef7000)libutil.so.1=>;/lib/x86_64-linux-gnu/libutil.so.1(0x00007f10ecac0000)libdl.so.2=>;/lib/x86_64-linux-gnu/libdl.so.2(0x00007f10ec8b0000)libm.。/usr/lib/x86_64-linux-gnu/libssl.so.1.1(0x00007f10ec280000)libcryp.so.1.1=>;/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1(0x00007f10ebdb0000)libc.so.6=>;/lib/x86_64-linux-gnu/libc.so.6(。/lib/x86_64-linux-gnu/libpthread.so.0(0x00007f10eb780000)。

然而,构建一个完全静态的二进制文件不是我能在几个小时内完成的,因为它需要掌握Gambit和Gerbil放在适当位置的包装器,而我一直遇到缺乏这方面的文档的问题。

在不久的将来,我将再次关注Fennel和Chicken,前者是因为它为我提供了针对其他架构的能力,而后者是因为我怀疑我将能够使用Chicke5.x构建完全独立(并且非常快)的可执行文件,并且我喜欢它的可移植性。

与此同时,似乎没有什么比“仅仅在英特尔架构上工作”更受欢迎的了,我将为我的小项目设置7.3。如果您想要快速入门并且想要文档非常详细的内容,我建议您使用它。

但之后我将深入研究Gerbil,因为从长远来看,它可能更适合我的需要-而且我已经开始为它构建多架构Docker容器,这样我就可以试用它了。

在其他新闻方面,我最近从VSCodeVim切换到amVim,因为我发现VSCodeVim随机挂起了编辑器。

但是,与这篇文章更相关的是,我在搜索基本的paredit模式(它作为一个单独的扩展发布)时遇到了Calva。

我仍然更喜欢vim的paredit(它现在已经连接到我的打字反射中了),但是它已经足够好了,值得一提。