使用 LLVM 框架创建一个工作编译器,第 1 部分
使用
LLVM 及其中间表示构建一个自定义编译器
LLVM(之前称为低级虚拟机)是一种非常强大的编译器基础架构框架,专门为使用您喜爱的编程语言编写的程序的编译时、链接时和运行时优化而设计。LLVM 可运行于若干个不同的平台之上,它以能够生成快速运行的代码而著称。
LLVM 框架是围绕着代码编写良好的中间表示 (IR) 而构建的。本文(由两部分组成的系列文章的第一部分)将深入讲解 LLVM IR 的基础知识以及它的一些微妙之处。在这里,您将构建一个可以自动为您生成 LLVM IR 的代码生成器。拥有一个 LLVM IR 生成器意味着您所需要的是一个前端以供插入您所喜爱的编程语言,而且这还意味着您拥有一个完整的流程(前端解析器 + IR 生成器 + LLVM 后端)。创建一个自定义编译器会变得更加简单。
开始使用 LLVM
在开始之前,在您的开发计算器上必须已经拥有已编译好的 LLVM(参阅 参考资料 获取相关链接)。本文中的示例均基于
LLVM V3.0。对于 LLVM 代码的后期生成和安装,最重要的两个工具是 llc 和 lli。
llc 和 lli
因为 LLVM 是一个虚拟机,所以它可能应该拥有自己的中间字节代码表示,不是吗?最后,您需要将 LLVM 字节代码编译到特定于平台的汇编语言中。然后您才能通过本机汇编程序和链接器来运行汇编代码,从而生成可执行的共享库等。您可以使用 llc 将
LLVM 字节代码转换成特定于平台的汇编代码(请参阅 参考资料,获取关于此工具的更多信息的链接)。对于
LLVM 字节代码的直接执行部分,不要等到在本机执行代码崩溃后才发现您的程序中有一个或两个 bug。这正是 lli 的用武之地,因为它可以直接执行字节代码。lli 可以通过解释器或使用高级选项中的即时
(JIT) 编译器执行此工作。请参阅 参考资料,获取关于 lli 的更多信息的链接。
llvm-gcc
llvm-gcc 是 GNU Compiler Collection (gcc) 的修改版本,可以在使用 -S 选项运行时会生成 LLVM 字节代码。然后您可以使用
-emit-llvmlli来执行这个已生成的字节代码(也称为 LLVM
汇编语言)。有关 llvm-gcc 的更多信息,请参阅 参考资料。如果您没有在自己的系统中预先安装
llvm-gcc,那么您应该能够从源代码构建它,请参阅 参考资料,获取分步指南的链接。
使用 LLVM 编写 Hello World
要更好地理解 LLVM,您必须了解 LLVM IR 及其微妙之处。这个过程类似于学习另一种编程语言。但是,如果您熟悉 C 语言和 C++ 语言以及它们的一些语法怪现象,那么在了解
LLVM IR 方面您应该没有太大的障碍。清单 1 给出了您的第一个程序,该程序将在控制台输出中打印
"Hello World"。要编译此代码,您可以使用 llvm-gcc。
清单 1. 看起来非常熟悉的 Hello World 程序
#include <stdio.h>
int main( )
{
printf("Hello World!\n");
}
要编译此代码,请输入此命令:
Tintin.local# llvm-gcc helloworld.cpp -S -emit-llvm
完成编译后,llvm-gcc 会生成 helloworld.s 文件,您可以使用 lli 来执行该文件,将消息输出到控制台。lli 的用法如下:
Tintin.local# lli helloworld.s
Hello, World
现在,先看一下 LLVM 汇编语言。清单 2 给出了该代码。
清单 2. Hello World 程序的 LLVM 字节代码
@.str = private constant [13 x i8] c"Hello World!\00", align 1 ;
define i32 @main() ssp {
entry:
%retval = alloca i32
%0 = alloca i32
%"alloca point" = bitcast i32 0 to i32
%1 = call i32 @puts(i8* getelementptr inbounds ([13 x i8]* @.str, i64 0, i64 0))
store i32 0, i32* %0, align 4
%2 = load i32* %0, align 4
store i32 %2, i32* %retval, align 4
br label %return
return:
%retval1 = load i32* %retval
ret i32 %retval1
}
declare i32 @puts(i8*)
理解 LLVM IR
LLVM 提供了一个详细的汇编语言表示(参阅 参考资料 获取相关的链接)。在开始编写我们之前讨论的自己的
Hello World 程序版本之前,有几个需知事项:
- LLVM 汇编语言中的注解以分号 (
;)
开始,并持续到行末。 - 全局标识符要以
@字符开始。所有的函数名和全局变量都必须以@开始。 - LLVM 中的局部标识符以百分号 (
%)
开始。标识符典型的正则表达式是[%@][a-zA-Z$._][a-zA-Z$._0-9]*。 - LLVM 拥有一个强大的类型系统,这也是它的一大特性。LLVM 将整数类型定义为
iN,其中 N 是整数占用的字节数。您可以指定
1 到 223- 1 之间的任意位宽度。 - 您可以将矢量或阵列类型声明为
[no.。对于字符串 "Hello World!",可以使用类型
of elements X size of each element][13,假设每个字符占用 1 个字节,再加上为 NULL 字符提供的 1 个额外字节。
x i8] - 您可以对 hello-world 字符串的全局字符串常量进行如下声明:
@hello。使用关键字
= constant [13 x i8] c"Hello World!\00"constant来声明后面紧跟类型和值的常量。我们已经讨论过类型,所以现在让我们来看一下值:您以c开始,后面紧跟放在双引号中的整个字符串(其中包括\0并以0结尾)。不幸的是,关于字符串的声明为什么需要使用c前缀,并在结尾处包含
NULL 字符和 0,LLVM 文档未提供任何解释。如果您有兴趣研究更多有关 LLVM 的语法怪现象,请参阅 参考资料,获取语法文件的链接。 - LLVM 允许您声明和定义函数。而不是仔细查看 LLVM 函数的整个特性列表,我只需将精力集中在基本要点上即可。以关键字
define开始,后面紧跟返回类型,然后是函数名。返回
32 字节整数的main的简单定义类似于:define。
i32 @main() { ; some LLVM assembly code that returns i32 } - 函数声明,顾名思义,有着重大的意义。这里提供了
puts方法的最简单声明,它是printf:declare的 LLVM 等同物。该声明以关键字
i32 puts(i8*)declare开始,后面紧跟着返回类型、函数名,以及该函数的可选参数列表。该声明必须是全局范围的。 - 每个函数均以返回语句结尾。有两种形式的返回语句:
ret或
<type> <value>ret。对于您简单的主例程,使用
voidret就足够了。
i32 0 - 使用
call <function来调用函数。注意,每个函数参数都必须放在其类型的前面。返回一个 6 位的整数并接受一个 36 位的整数的函数测试的语法如下:
return type> <function name> <optional function arguments>call。
i6 @test( i36 %arg1 )
这只是一个开始。您还需要定义一个主例程、一个存储字符串的常量,以及处理实际打印的 puts 方法的声明。清单
3 显示第一次尝试创建的程序。
清单 3. 第一次尝试创建手动编写的 Hello World 程序
declare i32 @puts(i8*)
@global_str = constant [13 x i8] c"Hello World!\00"
define i32 @main {
call i32 @puts( [13 x i8] @global_str )
ret i32 0
}
这里提供了来自 lli 的日志:
lli: test.s:5:29: error: global variable reference must have pointer type
call i32 @puts( [13 x i8] @global_str )
^
程序并未按预期的运行。发生了什么?如之前所提及的,LLVM 拥有一个强大的类型系统。因为 puts 期望提供一个指向 i8 的指针,并且您能传递一个 i8 矢量,这样 lli 才能快速指出错误。该问题的常用解决方法(来自 C 编程背景)是使用类型转换。这将您引向了
LLVM 指令getelementptr。请注意,您必须将 清单
3 中的 puts 调用修改为与 call 类似,其中
i32 @puts(i8* %t)%t 是类型 i8*,并且是 [13 的类型转换结果。(请参阅 参考资料,获取
x i8] to i8*getelementptr 的详细描述的链接。)在进一步探讨之前,清单
4 提供了可行的代码。
清单 4. 使用 getelementptr 正确地将类型转换为指针
declare i32 @puts (i8*)
@global_str = constant [13 x i8] c"Hello World!\00" define i32 @main() {
%temp = getelementptr [13 x i8]* @global_str, i64 0, i64 0
call i32 @puts(i8* %temp)
ret i32 0
}
getelementptr 的第一个参数是全局字符串变量的指针。要单步执行全局变量的指针,则需要使用第一个索引,即 i64。因为
0getelementptr 指令的第一个参数必须始终是 pointer 类型的值,所以第一个索引会单步调试该指针。0
值表示从该指针起偏移 0 元素偏移量。我的开发计算机运行的是 64 位 Linux,所以该指针是 8 字节。第二个索引 (i64) 用于选择字符串的第 0 个元素,该元素是作为
0puts 的参数来提供的。
创建一个自定义的 LLVM IR 代码生成器
了解 LLVM IR 是件好事,但是您需要一个自动化的代码生成系统,用它来转储 LLVM 汇编语言。谢天谢地,LLVM 提供了强大的应用程序编程接口 (API) 支持,让您可以查看整个过程(请参阅 参考资料,获取程序员手册的链接)。在您的开发计算机上查找
LLVMContext.h 文件;如果该文件缺失,那么可能是您安装 LLVM 的方式出错。
现在,让我们创建一个程序,为之前讨论的 Hello World 程序生成 LLVM IR。该程序不会处理这里的整个 LLVM API,但是接下来的代码样例会证明,适量位数的 LLVM API 很直观而且易于使用。
针对 LLVM 代码的链接
LLVM 提供了一款出色的工具,叫做 llvm-config(参阅 参考资料)。运行 llvm-config,获取需要传递至 g++ 的编译标志、链接器选项的
–cxxflagsllvm-config 以及
–ldflagsllvm-config,以便针对正确的 LLVM 库进行链接。在 清单
–ldflags
5 的样例中,所有的选项均需要传递至 g++。
清单 5. 通过 LLVM API 使用 llvm-config 构建代码
tintin# llvm-config --cxxflags --ldflags --libs \
-I/usr/include -DNDEBUG -D_GNU_SOURCE \
-D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS \
-D__STDC_LIMIT_MACROS -O3 -fno-exceptions -fno-rtti -fno-common \
-Woverloaded-virtual -Wcast-qual \
-L/usr/lib -lpthread -lm \
-lLLVMXCoreCodeGen -lLLVMTableGen -lLLVMSystemZCodeGen \
-lLLVMSparcCodeGen -lLLVMPTXCodeGen \
-lLLVMPowerPCCodeGen -lLLVMMSP430CodeGen -lLLVMMipsCodeGen \
-lLLVMMCJIT -lLLVMRuntimeDyld \
-lLLVMObject -lLLVMMCDisassembler -lLLVMXCoreDesc -lLLVMXCoreInfo \
-lLLVMSystemZDesc -lLLVMSystemZInfo \
-lLLVMSparcDesc -lLLVMSparcInfo -lLLVMPowerPCDesc -lLLVMPowerPCInfo \
-lLLVMPowerPCAsmPrinter \
-lLLVMPTXDesc -lLLVMPTXInfo -lLLVMPTXAsmPrinter -lLLVMMipsDesc \
-lLLVMMipsInfo -lLLVMMipsAsmPrinter \
-lLLVMMSP430Desc -lLLVMMSP430Info -lLLVMMSP430AsmPrinter \
-lLLVMMBlazeDisassembler -lLLVMMBlazeAsmParser \
-lLLVMMBlazeCodeGen -lLLVMMBlazeDesc -lLLVMMBlazeAsmPrinter \
-lLLVMMBlazeInfo -lLLVMLinker -lLLVMipo \
-lLLVMInterpreter -lLLVMInstrumentation -lLLVMJIT -lLLVMExecutionEngine \
-lLLVMDebugInfo -lLLVMCppBackend \
-lLLVMCppBackendInfo -lLLVMCellSPUCodeGen -lLLVMCellSPUDesc \
-lLLVMCellSPUInfo -lLLVMCBackend \
-lLLVMCBackendInfo -lLLVMBlackfinCodeGen -lLLVMBlackfinDesc \
-lLLVMBlackfinInfo -lLLVMBitWriter \
-lLLVMX86Disassembler -lLLVMX86AsmParser -lLLVMX86CodeGen \
-lLLVMX86Desc -lLLVMX86AsmPrinter -lLLVMX86Utils \
-lLLVMX86Info -lLLVMAsmParser -lLLVMARMDisassembler -lLLVMARMAsmParser \
-lLLVMARMCodeGen -lLLVMARMDesc \
-lLLVMARMAsmPrinter -lLLVMARMInfo -lLLVMArchive -lLLVMBitReader \
-lLLVMAlphaCodeGen -lLLVMSelectionDAG \
-lLLVMAsmPrinter -lLLVMMCParser -lLLVMCodeGen -lLLVMScalarOpts \
-lLLVMInstCombine -lLLVMTransformUtils \
-lLLVMipa -lLLVMAnalysis -lLLVMTarget -lLLVMCore -lLLVMAlphaDesc \
-lLLVMAlphaInfo -lLLVMMC -lLLVMSupport
LLVM 模块和上下文环境等
LLVM 模块类是其他所有 LLVM IR 对象的顶级容器。LLVM 模块类能够包含全局变量、函数、该模块所依赖的其他模块和符号表等对象的列表。这里将提供了 LLVM 模块的构造函数:
explicit Module(StringRef ModuleID, LLVMContext& C);
要构建您的程序,必须从创建 LLVM 模块开始。第一个参数是该模块的名称,可以是任何虚拟的字符串。第二个参数称为LLVMContext。LLVMContext 类有些晦涩,但用户足以了解它提供了一个用来创建变量等对象的上下文环境。该类在多线程的上下文环境中变得非常重要,您可能想为每个线程创建一个本地上下文环境,并且想让每个线程完全独立于其他上下文环境运行。目前,使用这个默认的全局上下文来处理
LLVM 所提供的代码。这里给出了创建模块的代码:
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
您要了解的下一个重要类是能实际提供 API 来创建 LLVM 指令并将这些指令插入基础块的类:IRBuilder 类。IRBuilder 提供了许多华而不实的方法,但是我选择了最简单的可行方法来构建一个
LLVM 指令,即使用以下代码来传递全局上下文:
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context);
准备好 LLVM 对象模型后,就可以调用模块的 dump 方法来转储其内容。清单
6 给出了该代码。
清单 6. 创建一个转储模块
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h" int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module* module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context); module->dump( );
}
运行 清单 6 中的代码之后,控制台的输出如下:
; ModuleID = 'top'
然后,您需要创建 main 方法。LLVM
提供了 llvm::Function 类来创建一个函数,并提供了 llvm::FunctionType 将该函数与某个返回类型相关联。此外,请记住,main 方法必须是该模块的一部分。清单
7 给出了该代码。
清单 7. 将 main 方法添加至顶部模块
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h" int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getInt32Ty(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); module->dump( );
}
请注意,您需要让 main 返回 void,这就是您调用 builder.getVoidTy() 的原因;如果 main 返回 i32,那么该调用会是builder.getInt32Ty()。在编译并运行 清单
7 中的代码后,出现的结果如下:
; ModuleID = 'top'
declare void @main()
您还尚未定义 main 要执行的指令集。为此,您必须定义一个基础块并将其与 main 方法关联。基础块 是
LLVM IR 中的一个指令集合,拥有将标签(类似于 C 标签)定义为其构造函数的一部分的选项。builder.setInsertPoint 会告知
LLVM 引擎接下来将指令插入何处。清单 8 给出了该代码。
清单 8. 向 main 添加一个基础块
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h" int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getInt32Ty(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry); module->dump( );
}
这里提供了 清单 8 的输出。请注意,由于现在已经定义了 main 的基础块,所以
LLVM 转储将 main 看作为是一个方法定义,而不是一个声明。非常酷!
; ModuleID = 'top'
define void @main() {
entrypoint:
}
现在,向代码添加全局 hello-world 字符串。清单
9 给出了该代码。
清单 9. 向 LLVM 模块添加全局字符串
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Support/IRBuilder.h" int main()
{
llvm::LLVMContext& context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("top", context);
llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType =
llvm::FunctionType::get(builder.getVoidTy(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module); llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry); llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n"); module->dump( );
}
在 清单 9 的输出中,注意 LLVM 引擎是如何转储字符串的:
; ModuleID = 'top'
@0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00"
define void @main() {
entrypoint:
}
现在您需要做的就是声明 puts 方法,并且调用它。要声明 puts 方法,则必须创建合适的 FunctionType*。从您的
Hello World 源始代码中,您知道 puts 返回了 i32 并接受 i8* 作为输入参数。清单
10 给出了创建 puts 的正确类型的代码。
清单 10. 声明 puts 方法的代码
std::vector<llvm::Type *> putsArgs;
putsArgs.push_back(builder.getInt8Ty()->getPointerTo());
llvm::ArrayRef<llvm::Type*> argsRef(putsArgs); llvm::FunctionType *putsType =
llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false);
llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType);
FunctionType::get 的第一个参数是返回类型;第二个参数是一个 LLVM::ArrayRef 结构,并且最后的 false 指明了后面未跟可变数量的参数。ArrayRef 结构与矢量相似,只是它不包含任何基础数据,并且主要用于包装诸如阵列和矢量等数据块。由于这个改变,输出显示将如 清单
11 所示。
清单 11. 声明 puts 方法
; ModuleID = 'top'
@0 = internal unnamed_addr constant [14 x i8] c"hello world!\0A\00"
define void @main() {
entrypoint:
}
declare i32 @puts(i8*)
剩下要做的是调用 main 中的 puts 方法,并从 main 中返回。LLVM
API 非常关注转换等操作:您需要做的是调用 puts 来调用builder.CreateCall。最后,要创建返回语句,请调用 builder.CreateRetVoid。清单
12 提供了完整的运行代码。
清单 12. 输出 Hello World 的完整代码
#include "llvm/ADT/ArrayRef.h"
#include "llvm/LLVMContext.h"
#include "llvm/Module.h"
#include "llvm/Function.h"
#include "llvm/BasicBlock.h"
#include "llvm/Support/IRBuilder.h"
#include <vector>
#include <string> int main()
{
llvm::LLVMContext & context = llvm::getGlobalContext();
llvm::Module *module = new llvm::Module("asdf", context);
llvm::IRBuilder<> builder(context); llvm::FunctionType *funcType = llvm::FunctionType::get(builder.getVoidTy(), false);
llvm::Function *mainFunc =
llvm::Function::Create(funcType, llvm::Function::ExternalLinkage, "main", module);
llvm::BasicBlock *entry = llvm::BasicBlock::Create(context, "entrypoint", mainFunc);
builder.SetInsertPoint(entry); llvm::Value *helloWorld = builder.CreateGlobalStringPtr("hello world!\n"); std::vector<llvm::Type *> putsArgs;
putsArgs.push_back(builder.getInt8Ty()->getPointerTo());
llvm::ArrayRef<llvm::Type*> argsRef(putsArgs); llvm::FunctionType *putsType =
llvm::FunctionType::get(builder.getInt32Ty(), argsRef, false);
llvm::Constant *putsFunc = module->getOrInsertFunction("puts", putsType); builder.CreateCall(putsFunc, helloWorld);
builder.CreateRetVoid();
module->dump();
}
结束语
在这篇初步了解 LLVM 的文章中,了解了诸如 lli 和 llvm-config 等
LLVM 工具,还深入研究了 LLVM 中间代码,并使用 LLVM API 来为您自己生成中间代码。本系列的第二部分(也是最后一部分)将探讨可以使用 LLVM 完成的另一项任务,即毫不费力地添加额外的编译传递。
参考资料
学习
- 获取官方的 LLVM 教程,了解有关 LLVM 的精彩简介。
- 参见 The Architecture of Open Source Applications 中的 Chris
Latner 的章节,获取有关 LLVM 开发的更多信息。 - 了解有关以下两种 LLVM 重要工具的更多信息:
llc和lli。 - 查找有关 llvm-gcc 工具的更多信息,并了解如何通过分步指南从源代码构建此工具 Building
llvm-gcc from Source。 - 在 LLVM Language Reference Manual 中阅读有关 LLVM 汇编语言的更多信息。
- 查看 LLVM 的 语法文件(/llvm/trunk/utils/llvm.grm
的日志),获取有关 LLVM 中全局字符串常量的更多信息。 - 在 "The Often Misunderstood GEP Instruction" 文件中了解有关
getelementptr指令 的更多信息。 - 研究 LLVM Programmer's Manual,这是有关 LLVM API
必不可少的资源。 - 阅读有关
llvm-config工具 的资料,了解
LLVM 编译选项。 - 在 developerWorks 中国网站 Linux 技术专区中,查找数百篇 指南文章和教程,还有下载、论坛,以及针对
Linux 开发人员和管理员的丰富资源。 - developerWorks 中国网站 Web 开发专区 专门研究涵盖各种基于 Web
解决方案的文章。 - 观看 developerWorks 演示中心,那里提供了包括面向初学者的产品安装和设置演示,以及为经验丰富的开发人员提供的高级功能。
- 随时关注 developerWorks 技术活动和网络广播。
- 访问 developerWorks Open source 专区获得丰富的
how-to 信息、工具和项目更新以及最受欢迎的文章和教程,帮助您用开放源码技术进行开发,并将它们与
IBM 产品结合使用。
获得产品和技术
- 以最适合您的方式 IBM 产品评估试用版软件:下载产品试用版,在线试用产品,在云环境下试用产品,或者在 IBM
SOA 人员沙箱 中花费几个小时来学习如何高效实现面向服务的架构。
使用 LLVM 框架创建一个工作编译器,第 1 部分的更多相关文章
- qt 中创建一个工作线程(例子)
当一个事件需要很长的处理时间,就创建一个工作线程,防止主界面卡死. 1.新建一个QT的gui项目,里面包含main.cpp,mainwindow.h,mainwindow.cpp,mainwindow ...
- CodeIgniter框架——创建一个简单的Web站点(include MySQL基本操作)
目标 使用 CodeIgniter 创建一个简单的 Web 站点.该站点将有一个主页,显示一些宣传文本和一个表单,该表单将发布到数据库表中. 按照 CodeIgniter 的术语,可将这些需求转换为以 ...
- ThreadPoolExecutor – Java Thread Pool Example(如何使用Executor框架创建一个线程池)
Java thread pool manages the pool of worker threads, it contains a queue that keeps tasks waiting to ...
- 使用Django REST框架创建一个简单的Api
Create a Simple API Using Django REST Framework in Python WHAT IS AN API API stands for application ...
- Selenium 使用Eclipse+TestNG创建一个Project中遇到的问题
继续之前的学习,对于一个没有太多计算机基础的人,刚学习selenium,最大的问题就是不知道该如何入手,最简单的办法就是录制脚本之后导入. 但是导入的时候也会出现一些问题,就是该导入到哪里?如何导入? ...
- [C#]使用Quartz.NET来创建定时工作任务
本文为原创文章.源代码为原创代码,如转载/复制,请在网页/代码处明显位置标明原文名称.作者及网址,谢谢! 开发工具:VS2017 语言:C# DotNet版本:.Net FrameWork 4.0及以 ...
- 认识Excel并创建一个excel(网址:http://poi.apache.org/)
需要导入的jar包: package com.huawei.excel; import java.io.FileOutputStream; import org.apache.poi.hssf.use ...
- Angular实战之使用NG-ZORRO创建一个企业级中后台框架(进阶篇)
前言: 上一篇文章我们讲了如何在创建的Angular项目中快速引入ng-zorro-antd企业中台组件库,并且快速构建后台管理页面框架模板.这一章主要介绍的是如何在创建好的后台管理页面框架的快速生成 ...
- 如何创建一个简单的C++同步锁框架(译)
翻译自codeproject上面的一篇文章,题目是:如何创建一个简单的c++同步锁框架 目录 介绍 背景 临界区 & 互斥 & 信号 临界区 互斥 信号 更多信息 建立锁框架的目的 B ...
- 依赖注入[5]: 创建一个简易版的DI框架[下篇]
为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...
随机推荐
- 图穷匕见-所有反DDD模式都是垃圾
本文书接上回<主观与客观,破除DDD凭经验魔咒>,关注公众号(老肖想当外语大佬)获取信息: 最新文章更新: DDD框架源码(.NET.Java双平台): 加群畅聊,建模分析.技术实现交流: ...
- PDF解析,还能做得更好
随着大模型文档智能应用逐渐步入正轨,文档解析类产品成为其中重要的一环.文档解析工具能够"唤醒"沉睡在PDF文件中的知识,将其转化为机器能够识别.读取的信息,将可用数据从txt.cs ...
- 在 ArkTS 中,如何有效地进行内存管理和避免内存泄漏?
ArkTS 是鸿蒙生态的应用开发语言,它在 TypeScript 的基础上进行了优化和定制,以适应鸿蒙系统的需求. 以下是在 ArkTS 中进行有效的内存管理和避免内存泄漏: 1. 使用 const ...
- 【赵渝强老师】Docker Swarm实现服务的滚动更新
一.什么是Docker Swarm? Docker Swarm是Docker官方提供的一款集群管理工具,其主要作用是把若干台Docker主机抽象为一个整体,并且通过一个入口统一管理这些Docker主机 ...
- VB.NET 在 Windows下通过WIn32API获取CPU和内存的使用率
.net 要获取CPU和内存的使用率,一般是通过 PerformanceCounter 或者 WMI 查询得到,但是如果操作系统经常不正常断电或者别的什么原因,让系统的性能计数器抽风了,可能就会造成初 ...
- 使用 Debugger 断点 如果打开了断点调试 就会跳转空白页面
<!DOCTYPE html> <html> <header> <title>test</title> </header> &l ...
- vue 中 slot 的使用方式,以及作用域插槽的用法
分类:插槽又分为匿名插槽.具名插槽以及作用域插槽 : 匿名插槽,我们又可以叫它单个插槽或者默认插槽 因为组件标签中间是不允许写内容的,但是可以插入 插槽 :template 标签 : 插槽的使用方法 ...
- .NET 工控网关 轻量级组态软件
前言 C# 工控网关 + 轻量级组态软件. 项目介绍 SharpSCADA 是一个开源项目,提供灵活且强大的解决方案,以满足工业自动化和监控的需求. 作为一个轻量级的工业控制网关和组态软件,Sharp ...
- QToss:基于.NET架构的跨境电商的工具,助力企业实现智能数据营销
2024年10月13日下午参加了一场在深圳举办的跨境电商大佬们的聚会,现场参加的人数上千人. 大会分享嘉宾中有位来自美国的,他告诉我们不用担心美国政府会把TikTok禁掉,TikTok在全世界都很受欢 ...
- oracle中排序分析函数row_number()、rank()、dense_rank() 的区别,与rownum的注意事项
row_number()产生的序号不会重复,即1.2.3... rank()产生的序号会重复,但是会跳号,出现1.2.2.4...的情况 dense_rank()产生的序号会重复,不会跳号,会出现1. ...
内容