作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Jakisa拥有超过15年的跨平台应用开发经验. 他的大部分技术专长是c++开发.
在我们探索使用c++创建轻量级编程语言的过程中, 我们在三周前开始创建我们的标记器, 然后我们在接下来的两周内实现表达式评估.
Now, 现在是时候打包并交付一种完整的编程语言了,它不会像成熟的编程语言那样强大,但将拥有所有必要的功能, 包括一个非常小的足迹.
我觉得很有趣的是,新公司在他们的网站上有FAQ部分,这些部分不回答那些经常被问到的问题,而是他们想被问到的问题. 我在这里也这么做. 关注我工作的人经常问我,为什么Stork不能编译成字节码,或者至少是一些中间语言.
我很高兴回答这个问题. 我的目标是开发一种易于与c++集成的小足迹脚本语言. 我对“小足迹”没有严格的定义,“但我想象的是一个足够小的编译器,可以移植到功能不那么强大的设备上,并且在运行时不会消耗太多内存.
我没有关注速度, 因为我认为如果你有一个时间紧迫的任务,你会用c++编程, 但如果你需要一些可扩展性, 那么像Stork这样的语言就很有用了.
我并不是说没有其他的, 可以完成类似任务的更好的语言(例如, Lua). 如果他们不存在,那将是真正的悲剧, 我只是给你一个这种语言用例的概念.
因为它将被嵌入到c++中, 我发现使用c++的一些现有特性而不是编写一个完整的生态系统来达到类似的目的是很方便的. 不仅如此,我还发现这种方法更有趣.
与往常一样,您可以在我的 GitHub page. 现在,让我们仔细看看我们的进展.
Up to this part, 鹳是一个部分完成的产品, 所以我没能看到它所有的缺点和缺陷. However, 因为它的形状更完整了, 我更改了前面介绍的内容:
function_lookup
in compiler_context
now. function_param_lookup
is renamed to param_lookup
to avoid confusion.call
method in runtime_context
that takes std::vector
of arguments, 存储旧的返回值索引, 将参数压入堆栈, 更改返回值索引, calls the function, 从堆栈中弹出参数, 恢复旧的返回值索引, 并返回结果. That way, 我们不需要保留返回值下标的堆栈, as before, 因为c++栈就是为这个目的服务的.compiler_context
通过调用其成员函数返回的 scope
and function
. 每个对象都会创建新的 local_identifier_lookup
and param_identifier_lookup
,并在析构函数中恢复旧状态.runtime_context
,由成员函数返回 get_scope
. 该函数在其构造函数中存储堆栈大小,并在析构函数中恢复堆栈大小.const
关键字和常量对象. 它们可能有用,但不是绝对必要的.var
关键字删除,因为它是目前不需要的.sizeof
关键字,它将在运行时检查数组大小. 也许一些c++程序员会觉得这个名字有点亵渎神明,就像c++一样 sizeof
runs in compile time, 但我选择这个关键字是为了避免与一些常见的变量名冲突——例如, size
.tostring
关键字,它显式地将任何内容转换为 string
. 它不能是一个函数,因为我们不允许函数重载.因为我们使用的语法与C及其相关的编程语言非常相似, 我只是给你一些细节,可能不太清楚.
变量类型声明如下:
void
,仅用于函数返回类型number
string
T[]
数组是什么类型的元素 T
R(P1,...,Pn)
是返回类型的函数吗 R
并接收各种类型的参数 P1
to Pn
. 每一种类型都可以加上 &
如果是通过引用传递的.函数声明如下: [public] function R name(P1 P1,…Pn Pn)
所以,它必须加上 function
. 如果前缀是 public
,然后可以从c++中调用它. 如果函数不返回值,它将计算为其返回类型的默认值.
We allow for
-循环,在第一个表达式中声明. We also allow if
-statement and switch
带有初始化表达式的-语句,如c++ 17. The if
-statement以an开头 if
-block,后面跟着0个或多个 elif
-块,可选的,一个 else
-block. 的初始化表达式中声明该变量 if
语句,它将在每个块中可见.
我们允许a后面有一个可选的数字 break
语句,可以从多个嵌套循环中中断. 所以你可以有下面的代码:
for (number i = 0; i < 100; ++i) {
for(number j = 0; j < 100; ++j) {
if (rnd(100) == 0) {
break 2;
}
}
}
而且,它将从两个循环中断开. 该数字在编译时进行验证. How cool is that?
这一部分增加了许多功能, 但如果我说得太详细, 我甚至可能会失去那些仍然在忍受我的最忠实的读者. 因此,我将有意跳过故事的一个非常大的部分-编译.
那是因为我已经在 first and second 本博客系列的部分内容. 我专注于表达式,但编译其他任何东西都没有太大不同.
不过,我要给你们举一个例子. This code compiles while
statements:
statement_ptr compile_while_statement (
compiler_context& ctx, tokens_iterator& it, possible_flow pf
)
{
Parse_token_value (ctx, it, reserved_token::kw_while);
Parse_token_value (ctx, it, reserved_token::open_round);
expression::ptr expr = build_number_expression(ctx, it);
Parse_token_value (ctx, it, reserved_token::close_round);
Block_statement_ptr block = compile_block_statement(ctx, it, pf);
返回create_while_statement(std::move(expr), std::move(block));
}
正如你所看到的,这一点也不复杂. It parses while
, then (
,然后构建一个数字表达式(我们没有布尔值),然后进行解析 )
.
之后,它编译一个可能在内部的块语句 {
and }
或不(是的,我允许单语句块),它创建一个 while
statement in the end.
您已经熟悉了前两个函数参数. The third one, possible_flow
,显示允许的流量改变命令(continue
, break
, return
)在我们正在解析的上下文中. 如果编译语句是某些函数的成员函数,我可以将这些信息保存在对象中 compiler
class, 但我不太喜欢大型课程, 编译器肯定就是这样一个类. 传递一个额外的参数, especially a thin one, will not hurt anyone, and who knows, 也许有一天我们能够并行化这些代码.
我想在这里解释编译的另一个有趣方面.
如果我们想要支持两个函数相互调用的场景, 我们可以用c语言:允许前向声明或有两个编译阶段.
我选择了第二种方法. 找到函数定义后,将其类型和名称解析为对象named incomplete_function
. Then, we will skip its body, 没有解释, 通过简单地计算大括号的嵌套级别,直到关闭第一个大括号. 我们会在这个过程中收集代币,保存在里面 incomplete_function
,并将函数标识符添加到 compiler_context
.
一旦我们传递了整个文件, 我们将完整地编译每个函数, 这样它们就可以在运行时被调用. 这样,每个函数都可以调用文件中的任何其他函数,并可以访问任何全局变量.
全局变量可以通过调用相同的函数来初始化, 一旦这些函数访问未初始化的变量,就会立即导致老的“鸡和蛋”问题.
如果发生这种情况,问题可以通过抛出一个 runtime_exception
那只是因为我很好. 坦率地说,访问违规是您编写这样的代码所能得到的最少的惩罚.
有两种类型的实体可以出现在全局作用域中:
每个全局变量都可以用返回正确类型的表达式初始化. 为每个全局变量创建初始化器.
每个初始化式返回 lvalue
,因此它们充当全局变量的构造函数. 当没有为全局变量提供表达式时,将构造默认初始化项.
This is the initialize
member function in runtime_context
:
无效runtime_context::initialize() {
_globals.clear();
for (const auto& 初始化器:_initializers) {
_globals.emplace_back(initializer->evaluate(*this));
}
}
它是从构造函数调用的. 它清除全局变量容器,因为它可以显式调用,以重置 runtime_context
state.
正如我前面提到的,我们需要检查是否访问了未初始化的全局变量. 因此,这是全局变量访问器:
variable_ptr& Runtime_context::global(int idx) {
runtime_assertion(
idx < _globals.size(),
“未初始化的全局变量访问”
);
return _globals[idx];
}
如果第一个参数的计算结果为 false
, runtime_assertion
throws a runtime_error
带有相应的消息.
每个函数都实现为捕获单个语句的lambda, 然后用 runtime_context
函数接收到的.
你可以从 while
语句编译, 编译器是递归调用的, 从block语句开始, 哪个代表整个函数的块.
下面是所有语句的抽象基类:
class statement {
语句(常量声明&) = delete;
Void操作符= const语句&) = delete;
protected:
语句()= default;
public:
虚拟流执行(runtime_context)& context) = 0;
Virtual ~语句()= default;
};
除了默认的函数之外,唯一的函数是 execute
上执行语句逻辑 runtime_context
and returns the flow
,它决定了程序逻辑的下一步走向.
枚举struct flow_type{
f_normal,
f_break,
f_continue,
f_return,
};
class flow {
private:
flow_type _type;
int _break_level;
Flow (flow_type type, int break_level);
public:
Flow_type type() const;
Int break_level() const;
静态流normal_flow();
静态流break_flow(int break_level);
静态流continue_flow();
静态流return_flow();
flow consume_break();
};
静态创建者函数是自解释的,我编写它们是为了防止不合逻辑 flow
with non-zero break_level
和类型不同 flow_type::f_break
.
Now, consume_break
是用一个更少的中断关卡创造一个中断流还是, 如果中断电平达到零, the normal flow.
现在,我们将检查所有语句类型:
类simple_statement:公共语句{
private:
expression::ptr _expr;
public:
simple_statement(expression::ptr expr):
_expr (std::移动(expr))
{
}
流执行(runtime_context& context) override {
_expr->evaluate(context);
回流:normal_flow ();
}
};
Here, simple_statement
语句是从表达式创建的吗. 每个表达式都可以编译为返回的表达式 void
, so that simple_statement
可以从中创造吗. As neither break
nor continue
or return
可以是表达的一部分, simple_statement
returns flow::normal_flow()
.
类block_statement:公共语句{
private:
std::vector _statements;
public:
block_statement(std::vector statements):
_statements (std::移动(语句))
{
}
流执行(runtime_context& context) override {
auto _ = context.enter_scope();
For (const statement_ptr& 语句:_statements) {
if (
flow f = statement->execute(context);
f.type() != flow_type::f_normal
){
return f;
}
}
回流:normal_flow ();
}
};
The block_statement
keeps the std::vector
of statements. 它一个接一个地执行它们. 如果它们都返回非正常流,则立即返回该流. 它使用RAII作用域对象来允许声明局部作用域变量.
类local_declaration_statement:公共语句{
private:
std::vector::ptr> _decls;
public:
local_declaration_statement(std::vector::ptr> decls):
_decls (std::移动(decls))
{
}
流执行(runtime_context& context) override {
for (const expression::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
回流:normal_flow ();
}
};
local_declaration_statement
计算创建局部变量的表达式,并将新的局部变量压入堆栈.
类break_statement:公共语句{
private:
int _break_level;
public:
break_statement (int break_level):
_break_level (break_level)
{
}
流执行(runtime_context&) override {
回流:break_flow (_break_level);
}
};
break_statement
在编译时是否评估了中断级别. 它只是返回与中断级别对应的流.
类continue_statement:公共语句{
public:
Continue_statement () = default;
流执行(runtime_context&) override {
回流:continue_flow ();
}
};
continue_statement
just returns flow::continue_flow()
.
类return_statement:公共语句{
private:
expression::ptr _expr;
public:
return_statement(expression::ptr expr) :
_expr (std::移动(expr))
{
}
流执行(runtime_context& context) override {
context.retval() = _expr->evaluate(context);
回流:return_flow ();
}
};
类return_void_statement:公共语句{
public:
Return_void_statement () = default;
流执行(runtime_context&) override {
回流:return_flow ();
}
};
return_statement
and return_void_statement
both return flow::return_flow()
. 唯一的区别是前者有一个表达式,它在返回之前求值为返回值.
类if_statement:公共语句{
private:
std::vector::ptr> _exprs;
std::vector _statements;
public:
if_statement(
std::vector::ptr> exprs,
std::vector statements
):
_exprs (std::移动(exprs)),
_statements (std::移动(语句))
{
}
流执行(runtime_context& context) override {
for (size_t i = 0; i < _exprs.size(); ++i) {
if (_exprs[i]->evaluate(context)) {
return _statements[i]->execute(context);
}
}
return _statements.back()->execute(context);
}
};
类if_declare_statement:公共if_statement {
private:
std::vector::ptr> _decls;
public:
if_declare_statement(
std::vector::ptr> decls,
std::vector::ptr> exprs,
std::vector statements
):
if_statement (std::移动(exprs), std::移动(语句)),
_decls (std::移动(decls))
{
}
流执行(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
返回if_statement:执行(上下文);
}
};
if_statement
,它是为1创建的 if
-block, zero or more elif
-blocks, and one else
块(可以为空),计算它的每个表达式,直到一个表达式的计算结果为 1
. 然后执行该块并返回执行结果. 如果没有表达式求值为 1
,它将返回最后一个(else
) block.
if_declare_statement
有声明的语句是if子句的第一部分吗. 它将所有声明的变量压入堆栈,然后执行其基类(if_statement
).
类switch_statement:公共语句{
private:
expression::ptr _expr;
std::vector _statements;
std::unordered_map _cases;
size_t _dflt;
public:
switch_statement(
expression::ptr expr,
std::vector statements,
std::unordered_map cases,
size_t dflt
):
_expr (std::移动(expr)),
_statements (std::移动(语句)),
_cases (std::移动(例)),
_dflt(dflt)
{
}
流执行(runtime_context& context) override {
auto it = _cases.find(_expr->evaluate(context));
for (
Size_t idx = (it == _cases.end() ? _dflt : it->second);
idx < _statements.size();
++idx
) {
switch (flow f = _statements[idx]->execute(context); f.type()) {
案例flow_type:: f_normal:
break;
案例flow_type:: f_break:
return f.consume_break();
default:
return f;
}
}
回流:normal_flow ();
}
};
类switch_declare_statement:公共switch_statement {
private:
std::vector::ptr> _decls;
public:
switch_declare_statement (
std::vector::ptr> decls,
expression::ptr expr,
std::vector statements,
std::unordered_map cases,
size_t dflt
):
_decls (std::移动(decls)),
Switch_statement (std::move(expr), std::move(statements), std::move(cases), dflt)
{
}
流执行(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
返回switch_statement:执行(上下文);
}
};
switch_statement
逐个执行它的语句, 但它首先跳到从表达式求值得到的适当索引. 如果它的任何语句返回非正常流,它将立即返回该流. If it has flow_type::f_break
,它将首先消耗一个break.
switch_declare_statement
允许在其头文件中声明. 它们都不允许在函数体中声明.
类while_statement:公共语句{
private:
expression::ptr _expr;
statement_ptr _statement;
public:
while_statement(expression::ptr expr, statement_ptr声明):
_expr (std::移动(expr)),
_statement (std::移动(声明)
{
}
流执行(runtime_context& context) override {
while (_expr->evaluate(context)) {
switch (flow f = _statement->execute(context); f.type()) {
案例flow_type:: f_normal:
案例flow_type:: f_continue:
break;
案例flow_type:: f_break:
return f.consume_break();
案例flow_type:: f_return:
return f;
}
}
回流:normal_flow ();
}
};
类do_statement:公共语句{
private:
expression::ptr _expr;
statement_ptr _statement;
public:
do_statement(expression::ptr expr, statement_ptr声明):
_expr (std::移动(expr)),
_statement (std::移动(声明)
{
}
流执行(runtime_context& context) override {
do {
switch (flow f = _statement->execute(context); f.type()) {
案例flow_type:: f_normal:
案例flow_type:: f_continue:
break;
案例flow_type:: f_break:
return f.consume_break();
案例flow_type:: f_return:
return f;
}
} while (_expr->evaluate(context));
回流:normal_flow ();
}
};
while_statement
and do_while_statement
当它们的表达式求值为时,都执行它们的体语句 1
. 如果执行返回 flow_type::f_break
,他们消费它并返回. If it returns flow_type::f_return
, they return it. 在正常执行或继续的情况下,它们不做任何事情.
It may appear as if continue
没有效果. 然而,内部语句受到了它的影响. 例如,如果是, block_statement
,它没有计算到最后.
I find it neat that while_statement
是用c++ while
, and do-statement
with the C++ do-while
.
类for_statement_base:公共语句{
private:
expression::ptr _expr2;
expression::ptr _expr3;
statement_ptr _statement;
public:
for_statement_base(
expression::ptr expr2,
expression::ptr expr3,
statement_ptr声明
):
_expr2 (std::移动(expr2)),
_expr3 (std::移动(expr3)),
_statement (std::移动(声明)
{
}
流执行(runtime_context& context) override {
for (; _expr2->evaluate(context); _expr3->evaluate(context)) {
switch (flow f = _statement->execute(context); f.type()) {
案例flow_type:: f_normal:
案例flow_type:: f_continue:
break;
案例flow_type:: f_break:
return f.consume_break();
案例flow_type:: f_return:
return f;
}
}
回流:normal_flow ();
}
};
类for_statement:公共for_statement_base {
private:
expression::ptr _expr1;
public:
for_statement(
expression::ptr expr1,
expression::ptr expr2,
expression::ptr expr3,
statement_ptr声明
):
for_statement_base(
std::move(expr2),
std::move(expr3),
std::move(statement)
),
_expr1 (std::移动(expr1))
{
}
流执行(runtime_context& context) override {
_expr1->evaluate(context);
返回for_statement_base:执行(上下文);
}
};
类for_declare_statement:公共for_statement_base {
private:
std::vector::ptr> _decls;
expression::ptr _expr2;
expression::ptr _expr3;
statement_ptr _statement;
public:
for_declare_statement (
std::vector::ptr> decls,
expression::ptr expr2,
expression::ptr expr3,
statement_ptr声明
):
for_statement_base(
std::move(expr2),
std::move(expr3),
std::move(statement)
),
_decls (std::移动(decls))
{
}
流执行(runtime_context& context) override {
auto _ = context.enter_scope();
for (const expression::ptr& decl : _decls) {
context.push(decl->evaluate(context));
}
返回for_statement_base:执行(上下文);
}
};
for_statement
and for_statement_declare
的实现类似于 while_statement
and do_statement
. 他们继承自 for_statement_base
类,它执行大部分逻辑. for_statement_declare
的第一部分创建时 for
-loop是变量声明.
这些都是我们有的语句类. 它们是我们功能的基石. When runtime_context
是创建的,它保留了这些函数吗. 如果用关键字声明函数 public
,它可以被称为名字.
以上就是Stork的核心功能. 我将描述的所有其他内容都是为了使我们的语言更有用而添加的.
数组是同构容器,因为它们只能包含单一类型的元素. 如果我们想要异构容器,马上就会想到结构.
然而,还有更普通的异构容器:元组. 元组可以保留不同类型的元素,但必须在编译时知道它们的类型. 这是一个在Stork中声明元组的例子:
[number, string] t = {22321, "Siveric"};
这声明了一对 number
and string
and initializes it.
初始化列表也可以用来初始化数组. 当初始化列表中的表达式类型与变量类型不匹配时, 将出现编译器错误.
因为数组是作为容器实现的 variable_ptr
,我们免费获得了元组的运行时实现. 当我们确保所包含变量的类型正确时,就是编译时.
对Stork用户隐藏实现细节并以一种更加用户友好的方式呈现语言将会很好.
这门课将帮助我们实现这个目标. 我提出它没有实现细节:
class module {
...
public:
template
void add_external_function (const char* name, std::function f);
template
Auto create_public_function_caller(std::string name);
Void load(const char* path);
Bool try_load(const char* path, std::ostream* err = nullptr) noexcept;
void reset_globals();
...
};
The functions load
and try_load
将从给定的路径加载并编译Stork脚本. 首先,他们中的一个可以扔 stork::error
,但是第二个程序将捕获它并在输出中打印它(如果提供的话).
The function reset_globals
要重新初始化全局变量吗.
The functions add_external_functions
and create_public_function_caller
应该在编译前调用吗. 第一个添加了一个可以从Stork调用的c++函数. 第二个创建可调用对象,该对象可用于从c++中调用Stork函数. 如果公共函数类型不匹配,将导致编译时错误 R(Args…)
在Stork脚本编译期间.
我添加了几个可以添加到Stork模块的标准函数.
空白add_math_functions(模块& m);
空白add_string_functions(模块& m);
空白add_trace_functions(模块& m);
空白add_standard_functions(模块& m);
下面是一个鹳脚本的例子:
函数空交换(号码& x, number& y) {
number tmp = x;
x = y;
y = tmp;
}
函数void quicksort(
number[]& arr,
number begin,
number end,
数字(数字,数字)比较
) {
if (end - begin < 2)
return;
Number pivot = arr[end-1];
number i = begin;
for (number j = begin; j < end-1; ++j)
If (comp(arr[j], pivot))
swap(&arr[i++], &arr[j]);
swap (&arr[i], &arr[end-1]);
quicksort(&arr, begin, i, comp);
quicksort(&arr, i+1, end, comp);
}
函数void sort(number[])& Arr, 数字(数字,数字)比较) {
quicksort(&Arr, 0, sizeof(Arr), comp);
}
函数number less(number x, number y) {
return x < y;
}
公共函数void main() {
number[] arr;
for (number i = 0; i < 100; ++i) {
Arr [sizeof(Arr)] = rnd(100);
}
trace(tostring(arr));
sort(&arr, less);
trace(tostring(arr));
sort(&arr, greater);
trace(tostring(arr));
}
Here is the C++ part:
#include
#include "module.hpp"
#包括“standard_functions.hpp"
int main() {
std::string path = __FILE__;
path = path.substr(0, path.Find_last_of ("/\\") + 1) + "test ..stk";
使用命名空间stork;
module m;
add_standard_functions (m);
m.add_external_function (
"greater",
std::function([](number x, number y){
return x > y;
}
));
auto s_main = m.create_public_function_caller("main");
if (m.try_load(path.c_str(), &std::cerr)) {
s_main();
}
return 0;
}
在编译之前将标准函数添加到模块中,并且将这些函数添加到模块中 trace
and rnd
是从鹳脚本中使用的吗. The function greater
也是作为展示添加的吗.
该脚本是从文件“test”加载的.Stk,”与“main”在同一个文件夹中.cpp” (by using a __FILE__
预处理器定义),然后是函数 main
is called.
在脚本中,我们生成一个随机数组,使用比较器按升序排序 less
然后用比较器降序 greater
, written in C++.
您可以看到,对于任何精通C(或从C派生的任何编程语言)的人来说,代码都是完全可读的。.
我想在Stork中实现很多特性:
缺乏时间和空间是我们尚未实施它们的原因之一. 我会试着更新我的 GitHub page 当我在业余时间实现新功能时,就会发布新版本.
我们创造了一种新的编程语言!
在过去的六周里,这占据了我大部分的业余时间, 但我现在可以编写一些脚本并查看它们的运行情况. 这是我前几天一直在做的事, 每次它意外坠毁时,我都抓挠我的光头. 有时,它是一个小虫子,有时是一个讨厌的虫子. At other times, though, 我感到很尴尬,因为这是一个我已经和全世界分享过的错误决定. 但每一次,我都会修复并继续编码.
在这个过程中,我学到了 if constexpr
这是我以前从未用过的. 我也更加熟悉了右值引用和完美的转发, 以及c++ 17中我不经常遇到的其他较小的特性.
代码并不完美——我永远不会这样说——但它已经足够好了, 它主要遵循良好的编程实践. 最重要的是,它有效.
决定从头开始开发一门新语言对普通人来说可能听起来很疯狂, 甚至对一个普通的程序员来说也是如此, 但这更有理由去做,并向自己证明你能做到. 就像解决一个难题是一个很好的大脑锻炼,以保持精神健康.
枯燥的挑战在我们的日常编程中很常见, 因为我们不能只挑选有趣的方面,不得不做严肃的工作,即使它有时很无聊. 如果你是一个专业的开发人员, 您的首要任务是向您的雇主交付高质量的代码,并将食物放在桌子上. 这有时会使您避免在业余时间编程,并且会降低您早期编程学校时代的热情.
如果你不需要,也不要失去热情. 如果你觉得有趣,就继续做下去,即使这件事已经完成了. 你不需要为找乐子找理由.
如果你能把它——甚至部分地——融入到你的专业工作中,对你有好处! 没有多少人有这样的机会.
这个部分的代码将被冻结在我的专用分支 GitHub page.
语句是计算机程序中可以执行的最小单位.
数组包含相同类型的元素,而元组可以包含不同类型的元素.
在一些编程语言中, 字节码是编译的结果, 由可由解释器执行的低级指令组成的.
Jakisa拥有超过15年的跨平台应用开发经验. 他的大部分技术专长是c++开发.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.