惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Latest news
Latest news
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
L
LINUX DO - 热门话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
V
V2EX
博客园 - 司徒正美
B
Blog RSS Feed
AWS News Blog
AWS News Blog
MyScale Blog
MyScale Blog
Scott Helme
Scott Helme
Cisco Talos Blog
Cisco Talos Blog
Last Week in AI
Last Week in AI
NISL@THU
NISL@THU
博客园 - Franky
P
Proofpoint News Feed
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
S
Schneier on Security
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
WordPress大学
WordPress大学
The Hacker News
The Hacker News
T
Threatpost
阮一峰的网络日志
阮一峰的网络日志
A
Arctic Wolf
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
Engineering at Meta
Engineering at Meta
罗磊的独立博客
T
The Blog of Author Tim Ferriss
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
SecWiki News
SecWiki News
云风的 BLOG
云风的 BLOG
美团技术团队
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Security Latest
Security Latest
C
Cyber Attacks, Cyber Crime and Cyber Security
B
Blog
S
Security Affairs

Mox的笔记库

2026PPoPP MLIR Tutorial学习 | Mox的笔记库 MacOS配置《明日方舟:终末地》 | Mox的笔记库 2025:向内生长 | Mox的笔记库 由mlir::ExecutionEngine引发的跨系统问题 | Mox的笔记库 WSL2配置Cuda-Tile环境记录(未完待续) | Mox的笔记库 Vibe Coding手搓项目记录 | Mox的笔记库 给Debian上包——以DuckDB为例 | Mox的笔记库 UCPD.sys事件存档 | Mox的笔记库 换新电脑之Mac mini M4从购买到配置 | Mox的笔记库 Mac配置MLX-C开发环境 | Mox的笔记库 RISC-V meets RDBMS——RISC-V架构上可运行数据库一览 | Mox的笔记库 DuckDB Sort实现调查 | Mox的笔记库 修复Redis在树莓派5上无法运行的问题 | Mox的笔记库 如何在MLIR中自定义类型并且输出运行 | Mox的笔记库 网站网络结构变更记录 | Mox的笔记库 EDBT25论文阅读:PhoebeDB——A Disk-Based RDBMS Kernel for High-Performance and Cost-Effective OLTP SIGMOD25论文阅读:BPF-DB:——A Kernel-Embedded Transactional Database Management System For eBPF Applications Apache Arrow Gandiva项目解析 | Mox的笔记库 VLDB24论文阅读:Cloud-Native Database Systems and Unikernels——Reimagining OS Abstractions for Modern Hardware NoisePage源码分析(未完待续) | Mox的笔记库 VLDB20论文阅读:Mainlining Databases——Supporting Fast Transactional Workloads on Universal Columnar Data File Formats VLDB17论文阅读:Relaxed Operator Fusion for In-Memory Databases:Making Compilation, Vectorization, and Prefetching Work Together At Last 论文阅读:How not to structure your database-backed web applications——a study of performance bugs in the wild SIGMOD24阅读:ROME——Robust Query Optimization via Parallel Multi-Plan Execution 文章阅读:First Past the Post-Evaluating Query Optimization in MongoDB SIGMOD文章阅读:Apache Calcite——A Foundational Framework for Optimized Query Processing Over Heterogeneous Data Sources VLDB23论文阅读:Analyzing the Impact of Cardinality Estimation on Execution Plans in Microsoft SQL Server SIGMOD22论文阅读:Efficient Massively Parallel Join Optimization for Large Queries VLDB论文阅读:Weaving Relations for Cache Performance VLDB22论文阅读:ConnectorX——Accelerating Data Loading From Databases to Dataframes 论文阅读:UniKraft-Fast, Specialized Unikernels the Easy Way 当DuckDB遇上RISC-V | Mox的笔记库 SIGMOD25论文阅读:An Elephant Under The Microscope——Analyzing The Interaction Of Optimizer Components In PostgreSQL 论文阅读:Compile-Time Analysis of Compiler Frameworks for Query Compilation VLDB23阅读:Bringing Compiling Databases to RISC Architectures SIGMOD24文章阅读:Query Compilation Without Regrets | Mox的笔记库 2024:拥挤年代的想象与创造 | Mox的笔记库 如何给自己的博客添加MLIR和LLVM IR语法高亮 | Mox的笔记库 博客重构:从Hexo到Astro | Mox的笔记库 VLDB19-Parsing Gigabytes of JSON per Second论文阅读 CIDR25:Runtime-Extensible Parsers阅读 | Mox的笔记库 SIGMOD24文章阅读:VeriTxn | Mox的笔记库 MLIR学习资料整理 | Mox的笔记库 VLDB23文章阅读——Exploiting Cloud Object Storage for High-Performance Analytics VLDB24——OLAP on Modern Chiplet-Based Processors走马观花阅读 如何让数据库中的Python跑的更快-VLDB22-YeSQL文章阅读 | Mox的笔记库 你好,世界! | Mox的笔记库 如何愉快的运行一个MLIR程序 | Mox的笔记库 让系统研究更有意义:HarmonyOS NEXT的教训和经验——讲座回顾 | Mox的笔记库 VLDB22:YeSQL文章阅读(已废弃) | Mox的笔记库 UNSW 24T3 COMP9336上课记录 | Mox的笔记库 Velox开发环境配置踩坑记录 | Mox的笔记库 LingoDB源码编译与分析 | Mox的笔记库 论文阅读:Declarative Sub-Operators for Universal Data Processing 论文阅读:Designing an Open Framework for Query Optimization and Compilation MLIR Toy Tutorial实践记录 | Mox的笔记库 2024年7月RSSHub开发体验 | Mox的笔记库 LLVM-Kaleidoscope实操踩坑记录 | Mox的笔记库 澳洲大学计算机硕士比较 | Mox的笔记库 论文阅读——CDUL:CLIP-Driven Unsupervised Learning for Multi-Label Image Classification CVPR2023-CLIP算法调研 | Mox的笔记库 论批量快速添加图片与视频水印的事 | Mox的笔记库 基于元信息写入的服务器压力测试 | Mox的笔记库 家庭组网IPv6+Mesh折腾 | Mox的笔记库 MjAyMw==,希望,前进与平庸之道 | Mox的笔记库 code-server初体验 | Mox的笔记库 从Nginx到Caddy | Mox的笔记库 RMM观察与初探 | Mox的笔记库 计算机网络课设——UDP/TCP/TLS Socket实验 | Mox的笔记库 JQuery的XSS初探 | Mox的笔记库 生产实习记录 | Mox的笔记库 Fedora-CoreOS配置与试用(2023年) | Mox的笔记库 Electron学习笔记 | Mox的笔记库 ServerSentEvent学习 | Mox的笔记库 报告翻译:容器云的安全挑战 | Mox的笔记库 Vagrant配置Metarget靶场环境 | Mox的笔记库 OpenAI-whisper折腾 | Mox的笔记库 202202,困惑,混乱与未曾设想之路 | Mox的笔记库 2022年Hack the box:Tier1免费区全解 | Mox的笔记库 Navidrome部署记录 | Mox的笔记库 长安杯2021-snake复现 | Mox的笔记库 报告概要翻译:OBFUSCATING C++ PROGRAMS VIA CONTROL FLOW FLATTENING 从零开始的Django CVE-2022-28346复现 | Mox的笔记库 2022CISCN(西北区赛)-The shinning | Mox的笔记库 Docker+QEMU+Arm64(Ubuntu)+环境配置(2022版) | Mox的笔记库 Arch Linux运行树莓派系统(2022年) | Mox的笔记库 2022CISCN初赛-ez_usb-复盘WriteUp | Mox的笔记库 Arch Linux迁移计划 | Mox的笔记库 Django事务使用 | Mox的笔记库 记录第一次EduSRC上报 | Mox的笔记库 Jetbrain问题应急处理 | Mox的笔记库 Celery5.2学习&配置 | Mox的笔记库 Waline部署记录 | Mox的笔记库 Frida hook初次实战 | Mox的笔记库 NodeMCU-MicroPython配置实录 | Mox的笔记库 Log4j2漏洞复现 | Mox的笔记库 2022长安“战疫”网络安全卫士守护赛回顾 | Mox的笔记库 2021年12月 Vivo千镜杯回顾 | Mox的笔记库 Windows的WSL2+Docker初探 | Mox的笔记库 Hexo部署安装全流程回顾 | Mox的笔记库
淦!MLIR输出Hello World不应该这么难! | Mox的笔记库
MocusEZ · 2025-01-20 · via Mox的笔记库

众所周知,大家学习一门编程语言的时候,应该都是从学习从控制台输出“Hello World”开始

这让培养了我们“输出一个数 比 让数进行计算 要更加容易的”认知

这种认知会在你学习完LLVM/MLIR后改变。实际情况是:IO是一种复杂的计算,单纯的运算要比控制输入输出要更容易上手😉

从直觉上来看, 让仅仅只会”计算”的TRM来支撑一个功能齐全的操作系统的运行还是不太现实的. 这给我们的感觉就是, 计算机也有一定的”功能强弱”之分, 计算机越”强大”, 就能跑越复杂的程序. 换句话说, 程序的运行其实是对计算机的功能有需求的. 在你运行Hello World程序时, 你敲入一条命令(或者点击一下鼠标), 程序就成功运行了, 但这背后其实隐藏着操作系统开发者和库函数开发者的无数汗水.

——Chapter 程序, 运行时环境与AM 《南京大学 计算机科学与技术系 计算机系统基础 课程实验》

但对于MLIR而言,输出“Hello world”确实是一个有些复杂的事情,让我们看看网上的大家是怎么做的

网上样例

MLIR Toy Tutorials

在MLIR官方的样例Toy Tutorials中,你会在Chapter 6中看到 toy.print 的从Toy Dialect下降到LLVM Dialect

/// Return a symbol reference to the printf function, inserting it into the

/// module if necessary.

static FlatSymbolRefAttr getOrInsertPrintf(PatternRewriter &rewriter,

ModuleOp module,

LLVM::LLVMDialect *llvmDialect) {

auto *context = module.getContext();

if (module.lookupSymbol<LLVM::LLVMFuncOp>("printf"))

return SymbolRefAttr::get("printf", context);

// Create a function declaration for printf, the signature is:

// * `i32 (i8*, ...)`

auto llvmI32Ty = IntegerType::get(context, 32);

auto llvmI8PtrTy =

LLVM::LLVMPointerType::get(IntegerType::get(context, 8));

auto llvmFnType = LLVM::LLVMFunctionType::get(llvmI32Ty, llvmI8PtrTy,

/*isVarArg=*/true);

// Insert the printf function into the body of the parent module.

PatternRewriter::InsertionGuard insertGuard(rewriter);

rewriter.setInsertionPointToStart(module.getBody());

rewriter.create<LLVM::LLVMFuncOp>(module.getLoc(), "printf", llvmFnType);

return SymbolRefAttr::get("printf", context);

}

当时我看到的时候是懵逼的,然后就直接略过了😑(见我之前的笔记

getOrInsertPrintf这个函数做的工作是:

先查看Module中有没有先前声明过printf()

如果有就直接用(SymbolRefAttr::get("printf", context);

如果没有的话,就声明printf()函数(返回一个32位的值,传入一个指针——即字符串)

然后进行rewrite,替换toy.printllvm.func printf()

有用信息就这些,后面就开始讲Conversion Patterns, Full Lowering, JIT生成

说那么多,你就会发现:你马上要把Toy Tutorial做完了,但这东西连Hello World如何输出都没讲清楚🤣

vector.print

一次偶然的机会,你看到Vector Dialect里面有一个PrintOp,最美妙的事情莫过于:这个Op不仅能输出向量,甚至连字符串也能输出😄

image-20250118195029884

顺着信息,你去Github搜索vector.print str "Hello, World!",并很幸运的看到了下面这段代码:

// RUN: mlir-opt %s -test-lower-to-llvm | \

// RUN: mlir-cpu-runner -e entry -entry-point-result=void \

// RUN: -shared-libs=%mlir_c_runner_utils,%mlir_runner_utils | \

// RUN: FileCheck %s

/// This tests printing (multiple) string literals works.

func.func @entry() {

// CHECK: Hello, World!

vector.print str "Hello, World!\n"

// CHECK-NEXT: Bye!

vector.print str "Bye!\n"

return

}

把这段复制下来,并试着按照注释执行了一下

mlir-opt-18 print-str.mlir -test-lower-to-llvm

得到以下内容,看清来不错👍,基本都lower到了 LLVM Dialect:

module {

llvm.mlir.global private constant @vector_print_str_0(dense<[66, 121, 101, 33, 10, 10, 0]> : tensor<7xi8>) {addr_space = 0 : i32} : !llvm.array<7 x i8>

llvm.func @printString(!llvm.ptr)

llvm.mlir.global private constant @vector_print_str(dense<[72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, 10, 10, 0]> : tensor<16xi8>) {addr_space = 0 : i32} : !llvm.array<16 x i8>

llvm.func @entry() {

%0 = llvm.mlir.addressof @vector_print_str : !llvm.ptr

llvm.call @printString(%0) : (!llvm.ptr) -> ()

%1 = llvm.mlir.addressof @vector_print_str_0 : !llvm.ptr

llvm.call @printString(%1) : (!llvm.ptr) -> ()

llvm.return

}

}

对于%mlir_c_runner_utils,%mlir_runner_utils是什么并没有什么头绪,但GPT给出了答案

echo "$(llvm-config --prefix)/lib/libmlir_c_runner_utils.so"

echo "$(llvm-config --prefix)/lib/libmlir_runner_utils.so"

参照GPT的提示对内容补齐

mlir-opt-18 print-str.mlir -test-lower-to-llvm | \

mlir-cpu-runner-18 -e entry -entry-point-result=void \

-shared-libs=/usr/lib/llvm-18/lib/libmlir_c_runner_utils.so,/usr/lib/llvm-18/lib/libmlir_runner_utils.so

控制台成功输出了Hello world!😍

image-20250118201436005

但需要指出:这个样例隐藏了太多实现细节,在你自己的项目中,你既不能到处添加vector.print——没人因为仅仅想要一个输出添加一整个Vector Dialect,你也不能随意使用mlir-cpu-runner(虽然他实际上就是LLVM JIT引擎)

样例总结

虽然以上两个案例都有很多地方让人困惑,但仔细想想,其中的关键点也已经显现:

MLIR而本身并不提供输出相关操作——这部分内容是LLVM负责实现的

只要处理好 自定义Dialect 下降到 LLVM Dialect的逻辑,并通过llvm.call调用外部的printf(),就可以完成输出!😲

精简下 mlir-hello,改一个只有一个Op的Dialect,且这个Op将会输出Hello world

完整代码已上传Github:mlir-hello-world

代码讲解

首先编写有关Diealect的TableGen文件(HelloDialect.td

def Hello_Dialect : Dialect {

let name = "hello";

let summary = "A hello out-of-tree MLIR dialect.";

let description = [{

This dialect is minimal example to implement hello-world kind of sample code

for MLIR.

}];

let cppNamespace = "::hello";

}

定义Op的TableGen文件(HelloOps.td

#ifndef HELLO_OPS

#define HELLO_OPS

include "HelloDialect.td"

include "mlir/Interfaces/SideEffectInterfaces.td"

def WorldOp : Hello_Op<"world", [Pure]> {

let summary = "print Hello, World";

let description = [{

The "world" operation prints "Hello, World", and produces

no results.

}];

}

#endif // HELLO_OPS

输出Hello world的关键点在于如何lower到LLVM,这点可看LowerToLLVM.cpp

和Toy tutorials功能一致的getOrInsertPrintf(),但将函数类型的声明单独独立为getPrintfType()

static mlir::LLVM::LLVMFunctionType

getPrintfType(mlir::MLIRContext *context) {

auto llvmI32Ty = mlir::IntegerType::get(context, 32);

auto llvmPtrTy = mlir::LLVM::LLVMPointerType::get(context);

auto llvmFnType = mlir::LLVM::LLVMFunctionType::get(llvmI32Ty, llvmPtrTy,

/*isVarArg=*/true);

return llvmFnType;

}

static mlir::FlatSymbolRefAttr

getOrInsertPrintf(mlir::PatternRewriter &rewriter, mlir::ModuleOp module) {

auto *context = module.getContext();

if (module.lookupSymbol<mlir::LLVM::LLVMFuncOp>("printf")) {

return mlir::SymbolRefAttr::get(context, "printf");

}

mlir::PatternRewriter::InsertionGuard insertGuard(rewriter);

rewriter.setInsertionPointToStart(module.getBody());

rewriter.create<mlir::LLVM::LLVMFuncOp>(module.getLoc(), "printf",

getPrintfType(context));

return mlir::SymbolRefAttr::get(context, "printf");

}

getOrCreateGlobalString()里,我们将Hello world文本设为全局的LLVMArray类型

static mlir::Value getOrCreateGlobalString(mlir::Location loc,

mlir::OpBuilder &builder,

mlir::StringRef name,

mlir::StringRef value,

mlir::ModuleOp module) {

// Create the global at the entry of the module.

mlir::LLVM::GlobalOp global;

if (!(global = module.lookupSymbol<mlir::LLVM::GlobalOp>(name))) {

mlir::OpBuilder::InsertionGuard insertGuard(builder);

builder.setInsertionPointToStart(module.getBody());

auto type = mlir::LLVM::LLVMArrayType::get(

mlir::IntegerType::get(builder.getContext(), 8), value.size());

global = builder.create<mlir::LLVM::GlobalOp>(

loc, type, /*isConstant=*/true, mlir::LLVM::Linkage::Internal, name,

builder.getStringAttr(value));

}

// Get the pointer to the first character in the global string.

mlir::Value globalPtr =

builder.create<mlir::LLVM::AddressOfOp>(loc, global);

mlir::Value cst0 = builder.create<mlir::LLVM::ConstantOp>(

loc, mlir::IntegerType::get(builder.getContext(), 64),

builder.getIntegerAttr(builder.getIndexType(), 0));

return builder.create<mlir::LLVM::GEPOp>(

loc, mlir::LLVM::LLVMPointerType::get(builder.getContext()),

global.getType(), globalPtr, mlir::ArrayRef<mlir::Value>({cst0, cst0}));

}

编写Pass的runOnOperation(),下面的内容应该理论上应该可以精简(不精简也能run就是了😀)

void HelloToLLVMLoweringPass::runOnOperation() {

mlir::LLVMConversionTarget target(getContext());

target.addLegalOp<mlir::ModuleOp>();

mlir::LLVMTypeConverter typeConverter(&getContext());

mlir::RewritePatternSet patterns(&getContext());

populateAffineToStdConversionPatterns(patterns);

populateSCFToControlFlowConversionPatterns(patterns);

mlir::arith::populateArithToLLVMConversionPatterns(typeConverter, patterns);

mlir::populateFinalizeMemRefToLLVMConversionPatterns(typeConverter, patterns);

mlir::cf::populateControlFlowToLLVMConversionPatterns(typeConverter,

patterns);

populateFuncToLLVMConversionPatterns(typeConverter, patterns);

patterns.add<hello::WorldOpLowering>(&getContext());

auto module = getOperation();

if (failed(applyFullConversion(module, target, std::move(patterns)))) {

signalPassFailure();

}

}

必须要点明是对hello.world进行改写和下降

patterns.add<hello::WorldOpLowering>(&getContext());

记得要把Pass注册到HelloPasses.h里头

namespace hello {

std::unique_ptr<mlir::Pass> createLowerToLLVMPass();

} // namespace hello

运行效果

准备好这样一段MLIR

func.func @main() {

"hello.world"() : () -> ()

return

}

如果是使用mlir::OpBuilder构建代码块的话,这段MLIR等价于下面代码:

mlir::OpBuilder builder(&context);

mlir::OwningOpRef<mlir::ModuleOp> module = mlir::ModuleOp::create(builder.getUnknownLoc());

// Generate Code Block

auto funcType = builder.getFunctionType({}, {});

auto func = builder.create<mlir::func::FuncOp>(builder.getUnknownLoc(), "main", funcType);

module->push_back(func);

auto entryBlock = func.addEntryBlock();

builder.setInsertionPointToStart(entryBlock);

builder.create<hello::WorldOp>(builder.getUnknownLoc());

builder.create<mlir::func::ReturnOp>(

builder.getUnknownLoc());

你可以在仓库里的hello_world.cpp得到验证

./build/bin/helloworld -emit=mlir

为什么提及这个部分呢?如果要从解析器开始的话,语法AST树最后都会转成mlir::OpBuilder构建的形式,才能进行各类Pass

对于这样一段MLIR,它会首先下降到LLVM Dialect(这条命令在做mlir-opt的工作)

./build/bin/hello-opt test/hello_world.mlir -emit=mlir-llvm

效果:

module {

llvm.mlir.global internal constant @hello_word_string("Hello, World! \0A\00") {addr_space = 0 : i32}

llvm.func @printf(!llvm.ptr, ...) -> i32

llvm.func @main() {

%0 = llvm.mlir.addressof @hello_word_string : !llvm.ptr

%1 = llvm.mlir.constant(0 : index) : i64

%2 = llvm.getelementptr %0[%1, %1] : (!llvm.ptr, i64, i64) -> !llvm.ptr, !llvm.array<16 x i8>

%3 = llvm.call @printf(%2) vararg(!llvm.func<i32 (ptr, ...)>) : (!llvm.ptr) -> i32

llvm.return

}

}

可以看到,确实生成了lvm.mlir.global internal constant这一字符串常量,并且声明了printf()的外部LLVM函数存在

再从LLVM Dialect下降到LLVM IR(这条命令在做mlir-translate的工作)

./build/bin/hello-opt test/hello_world.mlir -emit=llvm

效果:

; ModuleID = 'LLVMDialectModule'

source_filename = "LLVMDialectModule"

target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"

target triple = "x86_64-pc-linux-gnu"

@str = private unnamed_addr constant [15 x i8] c"Hello, World! \00", align 1

; Function Attrs: nofree nounwind

define void @main() local_unnamed_addr #0 {

%puts = tail call i32 @puts(ptr nonnull dereferenceable(1) @str)

ret void

}

; Function Attrs: nofree nounwind

declare noundef i32 @puts(ptr nocapture noundef readonly) local_unnamed_addr #0

attributes #0 = { nofree nounwind }

!llvm.module.flags = !{!0}

!0 = !{i32 2, !"Debug Info Version", i32 3}

到这一步,你已经可以用lli运行,但我在代码里也添加了相关JIT运行实现

./build/bin/hello-opt test/hello_world.mlir

通过这个方案,你就能得到Hello, World!的文本输出😊

image-20250119230251832

FAQ

Q:为什么不用mlir::ExecutionEngine::create生成JIT运行环境?(就像 Toy Tutorials Chapter 6 那样)

A:这是个好问题😂原因是我在Toy Tutorials以外的地方从来没有运行成功过这个函数,而且mlir::ExecutionEngine::create用的也还是LLVM ORC JIT提供的运行环境,那还不如直接使用LLVM ORC JIT,也能提供更加灵活的操作

前人の工作

https://github.com/Lewuathe/mlir-hello

我写的代码之从中精简出来的,这个仓库经常更新,用Github Action保持与LLVM主线匹配,有很多值得学习的地方😋

https://github.com/Yancey1989/mlir-hello-world

我写玩这篇文章后从Google搜到的(和我的仓库名称撞了),使用的流程是从AST开始,看完我这篇后,再去试试也不错😂(3年没更新,可能LLVM18跑不起来)

结语

如果你之前已经阅读完MLIR Toy Tutorial,那么这篇文章将有助于理清MLIR于LLVM的层次关系,以及两者在不同阶段所发挥的作用

对我而言,输出Hello World可能只是个执念罢了😂这内容确实要比我预想的要复杂,但整理的过程当中也有新的收获——这也许就是写Blog的乐趣所在。