[译] Rust标准库有些特殊,让我们改它

news2024/11/15 10:53:44

本篇是对 RustConf 2023中的The standard library is special. Let’s change that.这一视频的翻译与整理, 过程中为符合中文惯用表达有适当删改, 版权归原作者所有.


今天我将讨论Rust的标准库,更具体地说,是关于标准库有何特殊之处,以及为什么我们应该改变这一点。首先声明一下,任何团队的成员都没有看过这个演讲,一切都是我的观点,部分是基于观察,部分是理想化的。显然,理想化的部分不保证会发生。

那么,当我说标准库是特殊的时候,我是什么意思呢?更重要的是,为什么我应该关心呢?事实证明,标准库在很多方面都是特殊的。

首先,标准库在稳定版上使用了nightly特性。虽然大量这些特性计划被稳定化,但有些特性打算永远保持在nightly上。这些应该尽可能被移除。

标准库还能够绕过一致性。这也不是绝对必要的。目前core、alloc和std是三个独立的crate。如果它们被合并成一个单一的crate,绕过一致性的需求就会完全消除。

标准库另一种独特的方式是它有一个预导入(Prelude)。其他crate也有预导入,但它们不是真正的预导入,因为它们必须手动导入到每个模块中。

标准库最明显的显著特殊之处是它不包含在cargo中。虽然看似无关紧要,但它确实有实际影响。标准库没有任何公开暴露的特性标志,这些标志可以用于基于能力的库。除此之外,这还可以允许禁用文件系统或网络访问。

std也是为单个优化目标速度而构建的,而像嵌入式系统这样的用例可能希望合理地优化大小。另一个限制是std不能独立版本控制,也不能做出破坏性更改。有一种可能的未来,可以在不需要std 2.0的情况下做出破坏性更改,但我今天不会讨论这个。

最后,我们已经在使标准库变得不那么特殊了。我认为以一种有组织的方式, 而不是像多年来那样临时进行这项工作是有意义的。


好吧,所以我们想让标准库不那么特殊。当然,这肯定有限制,对吧?是的,但这引出了一个问题:什么是(对std库)必须特殊对待的内在因素?

首先是语言项(language items)。语言项是编译器出于各种原因需要了解的函数、trait、类型等。目前有130个这样的项。例如,Add trait是一个语言项,因为编译器需要了解它以支持加法运算符。其中许多是必要的,比如用于语法集成和Rust的运行时,即panic和分配。然而,语言项的数量一直在增加,部分原因是有些是不必要的。Result及其变体都是语言项。这样做的唯一原因是优化。它们直到2020年8月才成为语言项,当时添加它们是为了获得轻微的性能提升。那么,为什么不为所有像Result这样的枚举实现优化呢?这避免了对语言项的需求,并且普遍适用于整个生态系统。这将为所有crate带来性能优势,而不仅仅是标准库。


另一个必须特殊对待的项是编译器内部函数(compiler intrinsics)。顾名思义,内部函数是由编译器本身实现的函数。有232个这样的函数,每个后端都必须实现它们。有些存在是为了直接与硬件交互,其中许多仅仅用于原子操作。其他内部函数的存在是为了访问只有编译器才有的信息。类型有多大?这个问题不问编译器是不可能回答的。像语言项一样,许多编译器内部函数可以被移除。取浮点值的平方根是一个内部函数,但这可以重写为使用内联汇编。在64位x86架构上,它只是一条指令。我实际上已经验证了这是可能的,而且没有任何副作用。我轻易找到的其他例子有assume,它可以trivially用unreachable来实现,以及unlikely,它可以用likely来实现。


语言项编译器内部函数构成了必须予以特殊处理的内在因素的主干。出于这个原因,它们的数量应该缩减到最低限度,并移到它自己的crate中,这个crate将保持特殊。这将允许标准库的其余部分继续成为另一个(普通)crate的旅程得以继续。



在开始时,我提到这部分是基于观察的。这是因为让std变得不那么特殊并不是一个新想法。在这方面已经进行了相当多的工作,可以追溯到多年前。有一个工作组叫做"std aware Cargo",其目标是允许用户在本地构建标准库。这个的实验性实现确实存在于nightly上。存在一些bug,比如与代码覆盖率不兼容,但它在很大程度上是可用的。未来可以而且将会改进其人体工程学,因为有时可能会有点麻烦才能让你想要发生的事情发生。

在更具体的方面,还有diagnostic_on_unimplemented。这在标准库内部已经存在了相当长的时间,作为rust_c_on_unimplemented。当一个trait没有实现但预期会实现时(可能是由于trait约束),它改善了错误消息。将其从rust_c属性转移到diagnostic命名空间已经被RFC接受,目前部分实现了。

也许列表中最令人惊讶的是deprecated属性,它最初只用于标准库。它有一段很长的历史,始于Rust 1.0发布之前。原始实现被重命名为rust_c_deprecated,并只提供给标准库。引入了一个新的deprecated属性,它没有任何代码与rust_c_deprecated相同。

deprecated属性在Rust 1.0发布后约一年就稳定了,但rust_c_deprecated仍然存在。这种情况一直持续到2022年3月,当时我完全合并了这两个属性的前端,此时功能几乎相同。我说几乎是因为仍然有一个区别,那就是对已弃用项的建议。这允许作者指出一个已弃用的项被什么替换了。目前,这必须通过note字段完成,这需要最终用户阅读并手动去进行更改。在这个例子中,alpha字段已被弃用,转而使用beta。这通过一个新的suggestion字段表示,并提供建议。编译器将产生更好的诊断信息。它将清楚地显示对Alpha的调用必须替换为beta。顺便说一下,这个建议是机器可应用的,所以它可以与cargo fixrust analyzer一起使用。这个功能自2019年1月就已实现,当时它是rust_c_deprecated属性的一部分。当它与deprecated合并时,该功能在nightly上对所有人开放,这就是它目前的状态。

标准库独特的一个有趣之处是其依赖于未指定的行为。在标准库中有一条评论说"只有标准库可以做出这个保证"。在标准库中可以找到一些类似的评论。当我第一次遇到这样的评论时,我觉得很奇怪。为什么标准库要这样做呢?

在我为这次演讲做研究时,我发现了两种主要情况,在这些情况下,标准库依赖于对其他库不保证的行为。一种涉及Niche value optimization (中文一般称为利基值优化),这种情况不是未指定的行为,而是根本不保证能编译。另一种情况是标准库依赖于胖指针的大小和布局,这就是这条评论所指的。

译者注:

Niche value optimization(利基值优化)是Rust编译器使用的一种内存优化技术。这种优化利用了某些类型的"空隙"或"利基"来存储额外的信息,从而减少内存使用并可能提高性能。让我为您详细解释一下:

  1. 基本概念:
    在Rust中,某些类型可能有一些值永远不会被使用。这些未使用的值就构成了一个"利基"(niche)。

  2. 常见例子:
    最常见的例子是Option<&T>。在正常情况下,&T是一个非空指针,因此Option<&T>理论上需要一个额外的布尔值来表示Some或None。但是,由于&T永远不会是null,编译器可以使用null指针来表示None,从而节省了额外存储布尔值的空间。

  3. 优化过程:
    编译器识别出类型中的未使用值,并利用这些值来编码额外的信息,通常是用于表示枚举变体或Option的None值。

  4. 应用场景:
    除了Option<&T>,这种优化也适用于其他情况,如包含不可能值的整数类型(例如,用作数组索引的非负整数)。

  5. 优势:

    • 减少内存使用
    • 可能提高性能(因为减少了内存访问)
    • 使某些类型更适合在FFI(外部函数接口)中使用
  6. 限制:
    这种优化是由编译器自动完成的,开发者通常不需要(也不应该)手动干预这个过程。

如果您想了解更多细节或有具体的使用场景需要讨论,我很乐意为您进一步解释。

只有一个问题:两者都是未指定的。编译器对这段代码没有任何保证。如果指针的布局发生变化,行为会悄悄地改变。更糟糕的是,如果指针的大小发生变化,那将导致未定义行为,因为某些代码将执行越界读取。

虽然胖指针的大小一直是usize类型大小的两倍,但据我所知,这并没有被正式保证。布局也是如此,它从未改变,但并没有得到保证。这段代码之所以能存在,只是因为编译器与标准库耦合。就标准库而言,它的代码在当前行为下工作,这就足够了。话虽如此,要让标准库变得不那么特殊,有两个选择。一是将胖指针变成一个语言项,这将确保它由编译器实现,从而保持同步。另一个选择当然是简单地保证胖指针的大小和布局。在我看来,这是更可取的,因为它也将是朝着稳定ABI迈出的一小步但却是重要的一步。


接下来讨论一些仍在nightly上的东西,但与大多数nightly特性不同,它在标准库的公共API中暴露出来。这个特性是负面实现(negative implementations)。负面实现在功能上是一个承诺,永远不会实现某个trait。可能最常见的用例是想要选择退出自动trait,即Send和Sync。

这在技术上是可能的,但这样做相当不符合人体工程学,因为它需要一些hack。这怎么可能呢?嗯,标准库包含有这些trait的负面实现的类型。具体来说,MutexGuard实现了!Send, Cell实现了!Sync,可变指针实现了!Send和!Sync。这很好,除了你可能想避免在内存中存储这些类型。相反,你可以将它们包装在PhantomData中,PhantomData在运行时不存在,但如果任何人都可以不使用这个hack就退出自动trait,那肯定会更简单。

负面实现还有其他用途。它们对trait解析也很有用,因为它们允许看似重叠的实现。

例如,标准库有一个实现,允许任何错误类型转换为Box<dyn Error>。但如果我们也想为Box<dyn Error>实现From<String>呢?为此,我们需要一个图表。在顶部是可能实现Error的类型,无论是当前还是将来。左边是已经实现的类型,右边是永远不会实现的类型。所有类型都从图表的顶部开始,但作者可以选择向下移动到左边或右边。对所有Error类型的blanket实现考虑了所有可能实现Error的类型,无论它们当前是否实现。出于这个原因,第二行是被禁止的。String可能在将来实现Error。为了满足编译器中的重叠检查,我们必须明确承诺String永远不会实现Error。这样做,我们将String移到了图表的右侧,将它们从blanket实现中排除。

值得注意的是,所有作者都可以选择在图表中向下移动。然而,向上移动是一个破坏性的变化,因为它是删除了对编译器和其他用户做出的承诺。这对正面和负面实现都是如此。


Rust的一个期待已久的特性是特化(specialization)。特化是一个特性,在某种程度上允许重叠的实现。这里的限制是一个实现必须是另一个的子集。然而,这仍然允许非常有用的行为。

目前,Default只为长度最多为32的类型实现。这是因为长度为零的数组不需要Default约束,而所有其他长度都需要。有了特化,我们可以为所有长度有一个默认实现,特化长度为零以避免约束。虽然这看起来足够简单,但这个例子目前在nightly上不能编译。

虽然特化是一个强大的特性,但要正确实现它也非常困难。当前的实现已知是不健全的。有一个min_specialization试图避免这种不健全,但这仍然不够。

可能是由于困难,特化的工作基本上停滞了。特化可能需要具有相当多类型理论知识的人来提出一个健全的子集,这需要大量努力才能稳定。尽管困难,但特化目前用于优化, 所有用法都经过仔细检查,没有出现在公共API中。

提议特化的RFC于2015年7月发布,就在Rust 1.0发布两个月后。在RFC中,特化被描述为"trait系统的一个相对较小的扩展"。我想我们都同意这有点乐观了。特化可能是本次演讲中提到的最困难的项目。

虽然特化可能非常困难,但这里有一个不应该那么困难的:Prelude是由crate提供的,在每个模块中自动导入的东西。这就是让你可以使用Vec却不必手动导入它的原因。

标准Prelude的内容默认是隐式的。Alloc Prelude过去存在,但它被删除了,因为它的内容不是自动进入作用域的。

所以我有一个问题:为什么不让每个crate声明一个Prelude呢?虽然一些crate确实有它们称为Prelude的模块,但它们的内容不是自动进入作用域的。我想做的事情从设计角度来看相对简单。

首先,我们可以注解任何将成为Prelude一部分的项。任意数量的项可以被注解,它们不必在一个共享模块中。在这个例子中,我们也重命名了该项。这与普通的use语句相同,它被用来避免潜在的命名冲突。

如果我们想在某个位置排除Prelude怎么办? 没问题,模块可以根据需要选择退出。这可以通过在模块上放置注解并指示我们想要排除哪些crate的Prelude来实现。

也许最重要的问题是,什么阻止crate声明巨大的Prelude,破坏每个人的体验?这有一个简单的解决方案:留给最终用户。最终用户选择在Cargo.toml中使用哪些Prelude。用户必须明确选择使用给定crate的Prelude。这允许最大的灵活性,因为如果没有明确请求,什么都不会进入作用域。

虽然自定义Prelude最初在2015年2月被提出,但这与那个提案有显著偏差。我相信crate Prelude在适度使用时会提供显著的好处,像itertools和rayon这样的crate是极好的用例。


今天的最后一项是稳定性属性。这可能是标准库能做而其他库不能做的事情中最明显的方式。稳定性属性用于指示一个项是否稳定。例如,OnceCell是稳定的,稳定性属性指示了它之前可用的特性名称和该特性稳定的版本。

如果一个项不稳定呢?不稳定的项非常有用,因为它们允许crate在不提供保证的情况下实验API。LazyLock目前是不稳定的。属性显示了用于启用LazyLock使用的特性名称,更重要的是,它显示了issue编号。这允许用户准确知道在哪里查看当前状态,甚至更好地提供反馈。

值得注意的是,每个公共项如果在crate中的任何地方使用,都需要一个稳定性属性。这是为了确保一切要么是稳定的,要么是不稳定的。不可能既不是稳定也不是不稳定的。

话虽如此,不可能自由使用不稳定的项。不稳定的项是选择加入的,需要一个特性门。如果你尝试不正确地使用一个不稳定的项,你会得到一个编译器错误。避免这个错误的唯一方法是在crate层面添加一个特性门。

有一些边缘情况需要考虑,比如当一个不稳定的项在稳定上下文中使用时,比如trait约束。这是允许的,但绝对应该有一个lint来确保这是deliberate的,因为如果不小心处理,对下游用户来说会令人困惑。

stable和unstable属性处理一个项是否总体上稳定,但还有const_stable和const_unstable属性来处理函数是否保证是const函数。

稳定性属性在Rust世界中已经存在很长时间了。2014年10月,在一篇官方博客文章中说,库作者可以继续使用稳定性属性。自那以后已经过去了将近9年,然而与那篇文章相反,稳定性属性目前明确仅用于标准库。尝试在其他crate中使用它们会导致编译器发出警告。我认为是时候最终为每个人提供这个功能了,因为我们知道它非常有用。

这确实是很多内容。在这些方面实际上做了什么呢?令人惊讶的是,已经做了相当多的工作。与Cargo的集成正在由专门为此目的而存在的工作组进行。它在nightly上可用,处于可用状态。


减少语言项和内部函数的数量是一个目标,但还没有完成任何工作。我打算研究一些更简单的情况,包括前面提到的那些。我相信其中一些可以t被简单地消除。

deprecated项的建议没有正式提案,但它已经实现了一段时间。它在nightly上可用,在deprecated_suggestion特性标志下。在稳定之前可能需要解决几个点,但不应该需要太多努力。

对于标准库中的未指定行为,这幸运地范围非常小。需要就期望的解决方案进行讨论,但任何实现都会很快跟进。

负面实现在nightly上实现,但该特性有已知的bug和边缘情况,必须在稳定之前解决。

特化不幸停滞了。据我所知,没有人在积极致力于此。然而,毫无疑问地,人们存在对特化的渴望。可能需要有类型理论知识的人来取得进展, 鉴于此,没有明确的时间表来解决问题,更不用说稳定该特性了。

至于crate Prelude,我实际上正在写一个RFC。正如熟悉这个过程的人所知,这需要一段时间。在被接受之前,更不用说实现和稳定,将会有大量的反馈和修订。

在我完成crate Prelude的RFC后,我将开始为稳定性属性写一个。这些属性在标准库中广泛使用,所以我们拥有实现这一点的能力和知识。稳定性属性有已知的限制,在广泛可用之前应该解决,但这是一个可以解决的问题。

总的来说,今天提到的许多项目已经有了工作,尽管工作程度不同。有些需要正式提案,而其他需要主题专家。它们都需要额外的工作。我个人正在尽我所能将标准库的有用功能带给每个人。我希望你们能分享我对这个目标的热情,并尽可能地协助实现它。

让标准库变得不那么特殊将需要大量的时间和努力,但这是Rust项目的一个总体和长期目标,整个Rust社区都将从中受益。

最后,屏幕上有大量信息。你可以在许多平台上找到我,包括GitHub和Mastodon,我的用户名是JH_Pratt。如果你有兴趣赞助我的工作,请这样做。我向你保证,这将是值得的。谢谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1924764.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

探索 Prompt 的世界:让你的 AI 更智能

探索 Prompt 的世界&#xff1a;让你的 AI 更智能 引言什么是 Prompt&#xff1f;Prompt 的重要性如何编写有效的 Prompt1. 清晰明确2. 包含关键细节3. 提供上下文 实践中的 Prompt 技巧1. 多次迭代2. 实验不同风格3. 结合实际应用 总结 引言 随着人工智能&#xff08;AI&…

通过vm可以访问那些属性——06

1.通过vue实例都可以访问那些属性&#xff1f;&#xff08;通过vm都可以vm.什么&#xff09; vue实例中的属性很多。有的以$开始&#xff0c;有的以_开始。 所有以$开始的属性&#xff0c;可以看做是公开的属性&#xff0c;这些属性是提供给程序员使用的 所有以_开始的属性&…

PyTorch是使用GPU和CPU优化的深度学习张量库——torchvision

torchvision datasets torchvision.datasets 包含了许多标准数据集的加载器。例如&#xff0c;CIFAR10 和 ImageFolder 是其中两个非常常用的类。 CIFAR10 CIFAR10 数据集是一个广泛使用的数据集&#xff0c;包含10类彩色图像&#xff0c;每类有6000张图像&#xff08;5000张…

<数据集>夜间车辆识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;5000张 标注数量(xml文件个数)&#xff1a;5000 标注数量(txt文件个数)&#xff1a;5000 标注类别数&#xff1a;8 标注类别名称&#xff1a;[car, pedestrian, traffic light, traffic sign, bicycle, bus, truck…

Leetcode(经典题)day2

H指数 274. H 指数 - 力扣&#xff08;LeetCode&#xff09; 先对数组排序&#xff0c;然后从大的一头开始遍历&#xff0c;只要数组当前的数比现在的h指数大就给h指数1&#xff0c;直到数组当前的数比现在的h指数小的时候结束&#xff0c;这时h的值就是要返回的结果。 排序…

Ubuntu搭建Android架构so库交叉编译环境

目录 前言一、下载NDK并安装二、安装NDK三、配置交叉编译工具链四、编写交叉编译脚本 前言 需要将一些源码编译成Android可用的架构的so库 一、下载NDK并安装 https://developer.android.google.cn/ndk/downloads/ 二、安装NDK 将下载下来的android-ndk-r23b-linux.zip解压…

17099 周工作计划安排

这个问题可以通过动态规划来解决。我们可以定义一个数组d&#xff0c;其中d[i]表示第i周选择项目后&#xff0c;产生的最大效益和。然后我们可以通过比较选择低压项目和高压项目的效益&#xff0c;来更新d[i]。 以下是解题步骤&#xff1a; 1. 初始化数组&#xff1a;首先&am…

《Linux系统编程篇》认识在linux上的文件 ——基础篇

前言 Linux系统编程的文件操作如同掌握了一把魔法钥匙&#xff0c;打开了无尽可能性的大门。在这个世界中&#xff0c;你需要了解文件描述符、文件权限、文件路径等基础知识&#xff0c;就像探险家需要了解地图和指南针一样。而了解这些基础知识&#xff0c;就像学会了魔法咒语…

视频播放器的问题

<template><div class"app-container"><el-form :model"queryParam" ref"queryForm" :inline"true"><el-form-item label"题目ID&#xff1a;"><el-input v-model"queryParam.id" cle…

python:绘制一元三次函数的曲线

编写 test_x3_3x.py 如下 # -*- coding: utf-8 -*- """ 绘制函数 y x^33x4 在 -3<x<3 的曲线 """ import numpy as np from matplotlib import pyplot as plt# 用于正常显示中文标题&#xff0c;负号 plt.rcParams[font.sans-serif] […

免费的AI抠图工具 毫秒级抠图 离线可用 -鲜艺AI抠图

鲜艺AI抠图是一款免费的AI抠图工具&#xff0c;不登录、不联网&#xff0c;内嵌 AI 模型&#xff0c;快至毫秒级抠图&#xff0c;支持批量抠图&#xff0c;支持点击按钮选择图片、拖入图片、粘贴图片、粘贴图片链接、从网页拖入图片&#xff0c;支持Windows和macos&#xff0c;…

Linux:Linux网络总结(附下载链接)

文章目录 下载链接网络问题综合问题访问一个网页的全过程&#xff1f;WebSocket HTTPHTTP基本概念GET与POSTHTTP特性HTTP缓存技术HTTP的演变HTTP1.1 优化 HTTPSHTTP与HTTPS有哪些区别&#xff1f;HTTPS解决了HTTP的哪些问题&#xff1f;HTTPS如何解决的&#xff1f;HTTPS是如何…

【触想智能】安卓工控一体机在自助终端设备上的应用分析

随着科技的发展和人们对自动化系统的需求不断增强&#xff0c;自助终端设备已经成为日常生活非常常见的设备之一&#xff0c;例如自助售货机、自助点餐机、自助银行服务等。这些设备在使用中都需要一个可靠的、稳定的操作系统来支持其各项功能的实现。 因此&#xff0c;安卓工控…

Postman接口模拟请求工具使用技巧

Postman是一款非常强大的接口模拟请求工具&#xff0c;可以帮助开发者快速测试、调试API接口。下面集合实际使用过程中的经验&#xff0c;分享大家一些基础使用技巧&#xff1a; 1. 安装与启动&#xff1a;首先在官网&#xff08;Download Postman | Get Started for Free&…

Qt下使用OpenCV的鼠标回调函数进行圆形/矩形/多边形的绘制

文章目录 前言一、设置imshow显示窗口二、绘制圆形三、绘制矩形四、绘制多边形五、示例完整代码总结 前言 本文主要讲述了在Qt下使用OpenCV的鼠标回调在OpenCV的namedWindow和imshow函数显示出来的界面上进行一些图形的绘制&#xff0c;并最终将绘制好的图形显示在QLabel上。示…

html(抽奖设计)

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>抽奖</title><style type"text/css">* {margin: 0;padding: 0;}.container {width: 800px;height: 800px;border: 1px dashed red;position: absolut…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(四)-无人机系统(UAS)命令与控制(C2)通信用例

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

鸿蒙系统在服装RFID管理中的应用:打造智能零售新时代

​随着物联网技术的迅速发展&#xff0c;服装零售行业正面临着新的变革与挑战。鸿蒙系统作为新一代智能操作系统&#xff0c;结合RFID技术&#xff0c;为服装行业提供了高效、智能的管理解决方案。常达智能物联&#xff0c;作为RFID技术的领先企业&#xff0c;致力于将鸿蒙系统…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(五)-同时支持无人机和eMBB用户数据传输的用例

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

1、BOREDHACKERBLOG:社交网络

靶机&#xff1a;https://www.vulnhub.com/entry/boredhackerblog-social-network,454/ 参考&#xff1a;Vulnhub靶机&#xff1a;BOREDHACKERBLOG: SOCIAL NETWORK_boredhackerblog系列-CSDN博客 需要使用virtualbox。 先去官网下载了最新版的vietualbox&#xff0c;以及把这…