Ton的编译过程(上)

系列文章目录

FunC编写初始准备



预先准备

首先请大家跟着艾丽卡一步一步的完成FunC编写初始准备 这里面环境的搭建。
接下来,请做好下面的步骤,如果这里面有任何疑惑先别担心,我们后面会慢慢介绍:

让我们开始我们的项目设置之旅。首先,我们要为我们的项目创建一个文件夹。
请注意这里是我的linux环境,如果你们是windows环境下面的命令不起效果,也可以直接在vscode中创建文件

mkdir my_first_contract && cd my_first_contract

在这里插入图片描述看到这个左上角鼠标放置的地方,new file是创建文件,旁边的那个是创建文件夹,慢慢摸索就会了。

接下来,我们用一个包管理器来初始化一个package.json文件。艾丽卡将使用yarn,木森更喜欢使用npm,我会在接下来都演示一遍,注意,大家只需要使用一个工具就ok啦

yarn init

or

npm init

系统会提示你输入一些参数,但你可以简单地在每个提示时按回车键。完成后,我们的项目目录中应该会有一个package.json文件,它包含以下默认内容:

{
  "name": "my_first_contract",
  "version": "1.0.0",
  "main": "index.js", 
  "license": "MIT"
}

现在,我们来安装一些库。这些库都与TypeScript相关。

yarn add typescript ts-node @types/node @swc/core --dev

or

npm install typescript ts-node @types/node @swc/core --dev

接下来,我们在项目根目录创建一个tsconfig.json文件,并在其中放置以下配置:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "ts-node": {
    "transpileOnly": true,
    "transpiler": "ts-node/transpilers/swc"
  }
}

上面的内容有些多,但其实他们就是在编程中,一些关于语法上面的规定,比如说:

"resolveJsonModule": true

这个家伙,如果没有他的话,你将不能够在你的代码中轻松的引用josn文件的包
比如说未来的某一天会遇到这个

import {hex} from "../build/main.compiled.json"

你看这个里面的main.compiled.json可以被引用就是"resolveJsonModule": true的个功劳,剩下的我们会慢慢解释的。。。

我们还需要安装三个与TON区块链相关的库:

  • ton-core:实现了TON区块链的低级原语的核心库。
  • ton-crypto:用于构建TON区块链应用的加密原语。
  • @ton-community/func-js:TON FunC编译器。

安装这些库的命令如下:

yarn add @ton/core ton-crypto @ton-community/func-js --dev

or

npm install @ton/core ton-crypto @ton-community/func-js --dev

艾丽卡,现在我们已经为编写和编译我们的智能合约做好了准备。接下来,我们可以开始编写我们的FunC代码,并使用这些工具来编译它。这将是我们的魔法之旅的第一步!

第一个FunC合约

艾丽卡(专注地):“木森,这些符文和符号看起来好复杂啊。我们真的能通过这本魔法书来编写我们的智能合约吗?”

木森(认真地):“当然可以,艾丽卡。只要我们跟着这本魔法书的指引一步步来,就能编写出我们的智能合约。首先,我们要做的是理解这些符文的含义和如何正确地组合它们。”

艾丽卡(点头):“好的,木森。那我们先从哪里开始呢?”

木森(指着魔法书):“我们先从创建一个名为contracts的魔法空间开始,然后在里面放置我们的main.fc卷轴,这将是我们编写合约的载体。”

艾丽卡(兴奋地):“就像我们在魔法工作台上准备材料一样!那我们快开始吧,我已经迫不及待想要看到我们的合约变成现实了。”

木森(微笑):“没问题,艾丽卡。跟着我,我们一步步来。首先,我们要用这个咒语来创建我们的魔法空间和卷轴。”

他们一起念出了咒语,很快,contracts文件夹和main.fc文件就出现在了他们面前。木森和艾丽卡相视一笑,知道他们已经迈出了成功的第一步。接下来,他们将开始在main.fc卷轴上书写他们的智能合约符文。

mkdir contracts && cd contracts && touch main.fc

接下来,我们打开main.fc文件,并输入一些简单的FunC代码。这段代码定义了一个智能合约可以接收消息的函数。

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

}

现在,我们要编写一个编译脚本,这个脚本会使用@ton-community/func-js库来编译我们的智能合约代码。

首先,我们创建一个scripts文件夹,并在其中创建一个名为compile.ts的TypeScript文件。

mkdir scripts && cd scripts && touch compile.ts

然后,我们在项目的package.json文件中添加一个快捷方式,这样我们就可以通过一个命令来运行我们的编译脚本。

{
   //...your previous package.json contents
   "scripts": {
	"compile": "ts-node ./scripts/compile.ts"
   }
}

现在,我们打开compile.ts文件,并开始编写编译脚本。我们会导入一些必要的模块,比如fs用于文件操作,process用于控制脚本执行过程,Cell用于存储合约的字节码,以及compileFunc用于实际的编译功能。

import * as fs from "fs";
import process from "process";
import { Cell } from "@ton/core";
import { compileFunc } from "@ton-community/func-js";

async function compileScript() {

}

compileScript();

接下来,我们使用compileFunc函数来编译我们的智能合约。我们传递给它合约文件的路径,并在编译出错时退出脚本。

async function compileScript() {
  const compileResult = await compileFunc({
    targets: ["./contracts/main.fc"],
    sources: (x) => fs.readFileSync(x).toString("utf8"),
  });

  if (compileResult.status === "error") {
    process.exit(1);
  }
}
compileScript();

深入compileFunc的内部

compileFunc初探

  const compileResult = await compileFunc({
    targets: ["./contracts/main.fc"],
    sources: (x) => fs.readFileSync(x).toString("utf8"),
  });

让我们逐步解释这段代码:

  1. const compileResult = await compileFunc({ ... });:这里使用await关键字等待compileFunc函数的执行结果。await只能在异步函数(用async关键字声明的函数)中使用。compileResult变量将会存储编译过程的结果。

  2. targets: ["./contracts/main.fc"]:这个属性指定了编译的目标文件,即需要编译的FunC语言文件的路径。在这个例子中,文件路径是./contracts/main.fc,表示文件位于contracts目录下,并且文件名为main.fc

  3. sources: (x) => fs.readFileSync(x).toString("utf8"),:这是一个函数,作为compileFuncsources属性的值。这个函数负责提供编译器需要的源代码文件的内容。当编译器需要读取文件内容时,它会调用这个函数,并将文件路径作为参数x传递给它。

    • fs.readFileSync(x):使用Node.js的fs模块同步地读取文件内容。这里的x是文件路径。
    • .toString("utf8"):将读取到的文件内容(通常是一个Buffer对象)转换为UTF-8编码的字符串。

综合来看,这段代码的意思是:
1.等待compileFunc函数完成编译工作
2.编译的目标是./contracts/main.fc文件
3.编译过程中,编译器会通过sources函数获取需要编译的源代码文件的内容。

艾丽卡的疑惑

艾丽卡(眼睛闪闪发光):“木森,我有个想法!我们为什么不直接把源代码放到compileFunc里面呢?这样不是更方便吗?”

木森(微笑):“艾丽卡,你的想象力总是这么丰富。通常,compileFunc函数确实是通过文件路径来读取源代码的。这是因为编译器需要知道代码文件的确切位置。”

艾丽卡(好奇地):“但是我们能不能尝试一下直接传递代码呢?就像我们用魔法棒直接施法一样!”

木森(思考):“这的确是个有趣的点子。虽然compileFunc默认是设计来读取文件的,但我们或许可以找到一种方法,把源代码作为字符串传递给它。”

艾丽卡(兴奋地):“那我们快试试吧!我喜欢冒险和尝试新事物!”

木森(点头):“好主意!让我们开始这个新的探索。或许可以尝试创建一个临时文件,或者看看compileFunc是否有其他方式可以接受字符串输入。这样我们的编译过程可能会变得更加灵活和方便。”

艾丽卡(跳起来):“耶!一起探索新的魔法吧,木森!我们一定能找到答案的!”

木森(笑着):“但是,任何事物可不能盲目尝试,我们可以试着去阅读他的底层真正的了解他。”

艾丽卡(疑惑):“可是要怎么阅读呢?他就这么几句话。”

木森(一本正经):“从哪里来?到哪里去!”
将鼠标移动到我们引入compileFunc的那段包上面,选中他,便可以看到它的路径
在这里插入图片描述然后,按下F12,奇迹出现了。。

package.json

突然,屏幕一闪,他们的视野仿佛穿透了代码的表面,直接进入了compileFunc函数的底层世界
node_modules/@ton-community/func-js/dist/index.ts文件下。

艾丽卡(好奇地):“木森,我有点困惑。为什么我们导入的是@ton-community/func-js,但是实际上我们却来到了node_modules/@ton-community/func-js/dist/index.ts呢?”

木森(耐心地):“艾丽卡,这是因为在Node.js的世界里,每个包(package)都可以指定一个主入口文件。这个入口文件是当你导入包时,实际上会加载的文件。”

艾丽卡(挠头):“哦?那这个是怎么决定的呢?”

木森(指向package.json文件):“看,这里。每个包都有一个package.json文件,它包含了包的元数据。在这个文件中,有一个字段叫做main,它指定了包的主入口文件。”

艾丽卡(凑近看):“哇,我看到了,这里写着"main": "dist/index.js"。所以当我们导入@ton-community/func-js时,实际上是导入了这个dist/index.js文件。”

木森(点头):“没错,艾丽卡。这就是Node.js和npm(或yarn)的工作方式。它们会根据package.json中的main字段来确定加载哪个文件。”

艾丽卡(恍然大悟):“原来如此!那这个dist文件夹又是什么呢?”

木森(解释):“distdistribution的缩写,它通常用于存放构建或编译后的代码。在这个案例中,@ton-community/func-js包的作者可能将编译后的JavaScript代码放在了dist文件夹中,以便用户可以直接使用。”

艾丽卡(兴奋地):“这真是太神奇了!就像我们的魔法书,每一页都有它的作用,而这个package.json就像是目录,告诉我们该去哪里找到我们想要的魔法。”

木森(微笑):“Node.js包的内部结构就是这样来找到正确的入口文件。”

艾丽卡(好奇地):“木森,这个项目里既有JavaScript文件又有TypeScript文件,我们应该看哪一个呢?”

木森(耐心地):“艾丽卡,虽然TypeScript提供了类型安全和更现代的语法特性,但是在Node.js环境中,最终运行的代码都是JavaScript。因此,阅读JavaScript文件可以帮助我们更直接地理解代码是如何工作的。”

艾丽卡(思考):“那么,TypeScript文件就不重要了吗?”

木森(微笑):“并不是的。TypeScript文件在编译时会被转换成JavaScript。它提供了额外的类型检查和更清晰的代码结构,这对于开发大型应用程序和团队协作非常有帮助。但是,如果你想要快速理解代码的运行逻辑,直接阅读JavaScript文件会更直观。”

初览index.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileFunc = exports.compilerVersion = exports.latestCompiler = exports.FuncCompiler = exports.sourcesResolver = exports.arraySourceResolver = exports.mapSourceResolver = void 0;
const path_1 = require("./path");
const utils_1 = require("./utils");
require('./funcfiftlib.js');
const func_js_bin_1 = require("@ton-community/func-js-bin");
const mapSourceResolver = (map) => {
    return (path) => {
        if (path in map) {
            return map[path];
        }
        throw new Error(`Cannot find source file \`${path}\``);
    };
};
exports.mapSourceResolver = mapSourceResolver;
const arraySourceResolver = (arr) => {
    return (path) => {
        const entry = arr.find(e => e.filename === path);
        if (entry === undefined)
            throw new Error(`Cannot find source file \`${path}\``);
        return entry.content;
    };
};
exports.arraySourceResolver = arraySourceResolver;
const sourcesResolver = (sources) => {
    if (typeof sources === 'function')
        return sources;
    if (Array.isArray(sources))
        return (0, exports.arraySourceResolver)(sources);
    return (0, exports.mapSourceResolver)(sources);
};
exports.sourcesResolver = sourcesResolver;
const copyToCString = (mod, str) => {
    const len = mod.lengthBytesUTF8(str) + 1;
    const ptr = mod._malloc(len);
    mod.stringToUTF8(str, ptr, len);
    return ptr;
};
const copyToCStringPtr = (mod, str, ptr) => {
    const allocated = copyToCString(mod, str);
    mod.setValue(ptr, allocated, '*');
    return allocated;
};
const copyFromCString = (mod, ptr) => {
    return mod.UTF8ToString(ptr);
};
class FuncCompiler {
    constructor(funcWASMObject) {
        this.createModule = async () => await this.module({ wasmBinary: this.wasmBinary });
        this.compilerVersion = async () => {
            const mod = await this.createModule();
            const versionJsonPointer = mod._version();
            const versionJson = copyFromCString(mod, versionJsonPointer);
            mod._free(versionJsonPointer);
            return JSON.parse(versionJson);
        };
        this.validateVersion = async () => {
            const v = await this.compilerVersion();
            return v.funcVersion === this.inputFuncVersion;
        };
        this.compileFunc = async (compileConfig) => {
            const resolver = (0, exports.sourcesResolver)(compileConfig.sources);
            let targets = compileConfig.targets;
            if (targets === undefined && Array.isArray(compileConfig.sources)) {
                targets = compileConfig.sources.map(s => s.filename);
            }
            if (targets === undefined) {
                throw new Error('`sources` is not an array and `targets` were not provided');
            }
            const entryWithNoSource = targets.find(filename => {
                try {
                    resolver(filename);
                    return false;
                }
                catch (e) {
                    return true;
                }
            });
            if (entryWithNoSource) {
                throw new Error(`The entry point \`${entryWithNoSource}\` was not provided in sources.`);
            }
            const mod = await this.createModule();
            const allocatedPointers = [];
            const sourceMap = {};
            const sourceOrder = [];
            const callbackPtr = mod.addFunction((_kind, _data, contents, error) => {
                const kind = copyFromCString(mod, _kind);
                const data = copyFromCString(mod, _data);
                if (kind === 'realpath') {
                    const path = (0, path_1.normalize)(data);
                    allocatedPointers.push(copyToCStringPtr(mod, path, contents));
                }
                else if (kind === 'source') {
                    const path = (0, path_1.normalize)(data);
                    try {
                        const source = resolver(path);
                        sourceMap[path] = { content: source, included: false };
                        sourceOrder.push(path);
                        allocatedPointers.push(copyToCStringPtr(mod, source, contents));
                    }
                    catch (err) {
                        const e = err;
                        allocatedPointers.push(copyToCStringPtr(mod, 'message' in e ? e.message : e.toString(), error));
                    }
                }
                else {
                    allocatedPointers.push(copyToCStringPtr(mod, 'Unknown callback kind ' + kind, error));
                }
            }, 'viiii');
            const configStr = JSON.stringify({
                sources: targets,
                optLevel: compileConfig.optLevel || 2,
            });
            const configStrPointer = copyToCString(mod, configStr);
            allocatedPointers.push(configStrPointer);
            const resultPointer = mod._func_compile(configStrPointer, callbackPtr);
            allocatedPointers.push(resultPointer);
            const retJson = copyFromCString(mod, resultPointer);
            // Cleanup
            allocatedPointers.forEach(ptr => mod._free(ptr));
            mod.removeFunction(callbackPtr);
            const snapshot = [];
            for (let i = sourceOrder.length - 1; i >= 0; i--) {
                const path = sourceOrder[i];
                if (sourceMap[path].included)
                    continue;
                snapshot.push({
                    filename: path,
                    content: sourceMap[path].content,
                });
            }
            const ret = JSON.parse(retJson);
            return {
                ...ret,
                snapshot,
            };
        };
        if (!('schemaVersion' in funcWASMObject))
            throw new Error('FunC WASM Object does not contain schemaVersion');
        if (funcWASMObject.schemaVersion !== 1)
            throw new Error('FunC WASM Object is of unknown schemaVersion ' + funcWASMObject.schemaVersion);
        const normalObject = funcWASMObject;
        this.module = normalObject.module;
        this.wasmBinary = (0, utils_1.base64Decode)(normalObject.wasmBase64);
        this.inputFuncVersion = normalObject.funcVersion;
    }
}
exports.FuncCompiler = FuncCompiler;
exports.latestCompiler = new FuncCompiler(func_js_bin_1.object);
exports.compilerVersion = exports.latestCompiler.compilerVersion;
exports.compileFunc = exports.latestCompiler.compileFunc;

exports.compileFunc = exports.latestCompiler.compileFunc;艾里卡结尾是这个

木森(认真地):“艾丽卡,这行代码告诉我们compileFunc实际上是从另一个模块 latestCompiler 中导出的。这意味着compileFunc函数的实现是在latestCompiler这个模块里。”

艾丽卡(好奇地):“那么,latestCompiler是什么呢?”

木森(解释):“latestCompiler很可能是一个包含了最新编译器实现的模块。在这个上下文中,exports.compileFunc = exports.latestCompiler.compileFunc; 这行代码表示我们将latestCompiler模块中的compileFunc函数导出为当前模块的一个公共接口。”

艾丽卡(思考):“所以,当我们在代码中调用compileFunc时,实际上是在调用latestCompiler中的compileFunc?”

木森(点头):“正是这样。这是一种常见的模块化编程技巧,它允许我们将功能封装在不同的模块中,然后在需要的时候将它们组合起来。”

艾丽卡(兴奋地):“那么,我们怎样才能了解更多关于latestCompiler的信息呢?”

木森(指导):“我们可以尝试查看latestCompiler模块的文档或者源代码。通常,这些信息可以在模块的package.json文件中找到,或者在模块的根目录下有相关的文档文件。”

艾丽卡(兴奋地):“木森,我找到latestCompiler的定义了!它是通过创建一个新的FuncCompiler实例来实现的,代码是exports.latestCompiler = new FuncCompiler(func_js_bin_1.object);。”

木森(点头):“很好,艾丽卡!这行代码的意思是,我们正在使用FuncCompiler类来创建一个编译器的实例,并将其赋值给latestCompiler。这样,我们就可以使用这个实例来调用编译功能了。”

艾丽卡(好奇地):“那func_js_bin_1.object又是什么呢?”

木森(解释):“func_js_bin_1.object通常是一个包含编译器所需的WebAssembly模块和其他信息的对象。这个对象可能是在@ton-community/func-js-bin包中定义的,它提供了编译器的底层实现。”

艾丽卡(思考):“所以,latestCompiler就是我们用来编译FunC代码的工具,而FuncCompiler类则负责处理编译的具体逻辑,对吗?”

木森(微笑):“正是如此,艾丽卡。通过创建FuncCompiler的实例,我们可以调用它的方法,比如compileFunc,来编译我们的智能合约。”

随后艾丽卡使用F12点开了func_js_bin_1.object

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.object = void 0;
exports.object = {
    schemaVersion: 1,
    funcVersion: '0.4.4',
    module: require('./funcfiftlib.js'),
    wasmBase64: require('./funcfiftlib.wasm.js').FuncFiftLibWasm,
};

艾丽卡(眼睛一亮):“木森,我找到了@ton-community/func-js-bin包里的宝藏!这里有FuncWASMObject类型和object常量。”

木森(微笑):“太棒了,艾丽卡!让我们来仔细看看这些宝藏是什么。”

艾丽卡(好奇地):“这个FuncWASMObject类型定义了什么呢?”

木森(解释):“FuncWASMObject类型定义了一个对象的结构,这个对象包含了编译器需要的所有信息。它有以下几个属性:

schemaVersion:这个数字表示对象结构的版本,这里固定为1,表示我们使用的是这个特定版本的结构。

funcVersion:这是一个字符串,表示编译器的版本。

module:这个属性是一个任意类型(any),它可能包含了与编译器模块相关的一些底层实现或引用。

wasmBase64:这是一个字符串,包含了编译器WebAssembly模块的Base64编码数据。”

艾丽卡(恍然大悟):“我明白了!那么object常量就是FuncWASMObject类型的一个实例,它提供了这些信息给FuncCompiler。”

木森(点头):“没错,艾丽卡。object常量就是FuncCompiler类在创建实例时所需要的那个func_js_bin_1.object。它实际上是一个已经准备好的编译器对象,我们可以直接用它来编译FunC代码。”

艾丽卡(困惑地):“木森,这个funcfiftlib.wasm.js文件里的内容看起来好奇怪啊,这串长长的编码是什么?它不像是我们平时看到的代码。”

module.exports = { FuncFiftLibWasm: 'AGFzbQEAAAAB0gZhYAF/AGABfwF/YAJ/fwF/YAN/f38Bf2ACf38AYAR/f39/AX9gA39/fwBgBX9/f39/AX9gBH9/f38AYAZ/f39/f38Bf2AHf39/f39/fwF/YAABf2AFf39/f38AYAZ/f39/f38AYAAAYAh/f39/f39/fwF/YAd/f39/f39/AGAJf39/f39/f39/AX9gAn9/AX5gAX8BfmAIf39/f39/f38AYAp/f39/f39/f39/AX9gAn9+AGAFf39+f38AYAJ/fgF/YAV/f39/fgF/YAt/f39/f39/f39/fwF/YAN/fn8Bf2ADf39+AGAKf39/f39/f39/fwBgBX9+fn5+AGAMf39/f39/f39/f39/AX9gCX9/f39/f39/fwBgAAF+YAN+f38AYAN/fn8BfmADf39+AX9gBH9/f34AYAF+AX5gBH9/f34Bf2AFf39/f3wBf2AAAXxgBH9+fn8AYAN/f38BfmADf35/AGAEf39/fwF+YAt/f39/f39/f39/fwBgAn5/AX5gAn5+AX5gA39+fgF+YAF+AX9gAX8BfGAGf39/f35/AX9gBn98f39/fwF/YAJ+fwBgBn9/f35+fgBgBX9/f35+AGAEf39+fgBgBn9/fn5/fwBgD39/f39/f39/f39/f39/fwBgB39/f39/fn4Bf2AGf39/f35+AX9gEH9/f39/f39/f39/f39/f38AYAR/f39/AXxgBH9/f38BfWAGf39/f398AX9gCH9/f39/fn9/AGAGf39+f39/AX9gEH9/f39/f39/f39/f39/f38Bf2ADf35+AGAEf39+fwF/YAd/f39/f35/AX9gBH9/fn8AYAJ/fABgDX9/f39/f39/f39/f38Bf2AOf39/f39/f39/f39/f38Bf2AEfn5+fgF/YAJ+fwF/YAp/fn5+fn5+fn5+AX9gAn5+AXxgBH9/f34BfmABfAF+YAl/f39/fH9/f38Bf2AJf39/f35/f39/AX9gBX9/f39/AX5gBn9/f39/fgF/YAJ+fgF9YAN+fn4Bf2ACfH8BfGARf39/f39/f39/f39/f39/f38Bf2ADf398AX9gBX9/f35/AX9gC39/f39+fn5/f39/AX9gAXwBf2ACf34BfmADf39/AXxgA39

木森(耐心地):“艾丽卡,你发现的这个其实是WebAssembly模块的Base64编码。这个编码是将二进制的WebAssembly模块转换成了文本格式,这样它就可以被方便地在网络中传输了。”

艾丽卡(好奇地):“但是,我们怎么从这个编码中得到真正的编译器呢?”

木森(解释):“在Node.js中,我们可以使用内置的Buffer类来解码这个Base64字符串,将其转换回二进制格式,然后加载到WebAssembly实例中。这个过程通常是由@ton-community/func-js库内部处理的。”

艾丽卡在index中找到了类似的base64的代码
this.wasmBinary = (0, utils_1.base64Decode)(normalObject.wasmBase64);
在里面他发现了编码的详细内容

"use strict";
// Credits: https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_–_rewriting_atob_and_btoa_using_typedarrays_and_utf-8  
Object.defineProperty(exports, "__esModule", { value: true });
exports.base64Decode = void 0;
function b64ToUint6(nChr) {
    return nChr > 64 && nChr < 91
        ? nChr - 65
        : nChr > 96 && nChr < 123
            ? nChr - 71
            : nChr > 47 && nChr < 58
                ? nChr + 4
                : nChr === 43
                    ? 62
                    : nChr === 47
                        ? 63
                        : 0;
}
function base64Decode(sBase64) {
    const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, "");
    const nInLen = sB64Enc.length;
    const nOutLen = (nInLen * 3 + 1) >> 2;
    const taBytes = new Uint8Array(nOutLen);
    let nMod3;
    let nMod4;
    let nUint24 = 0;
    let nOutIdx = 0;
    for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
        nMod4 = nInIdx & 3;
        nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
        if (nMod4 === 3 || nInLen - nInIdx === 1) {
            nMod3 = 0;
            while (nMod3 < 3 && nOutIdx < nOutLen) {
                taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
                nMod3++;
                nOutIdx++;
            }
            nUint24 = 0;
        }
    }
    return taBytes;
}
exports.base64Decode = base64Decode;

木森(微笑):“艾丽卡,这段代码是一个用来将Base64编码的字符串转换成二进制数据的函数。让我们一步步来看看它是如何工作的。”

艾丽卡(好奇地):“好的,木森。这个base64Decode函数是做什么的?”

木森(解释):“base64Decode函数接受一个Base64编码的字符串作为输入,然后返回相应的二进制数据。这个二进制数据被存储在一个Uint8Array类型的数组中。”

艾丽卡(思考):“那么,这个转换过程具体是怎么进行的呢?”

木森(指着代码):“首先,这个函数定义了一个辅助函数b64ToUint6,它将Base64编码中的每个字符映射到一个6位的数字上。Base64编码使用64个字符,所以每个字符可以表示6位信息。”

艾丽卡(专注地):“我看到了,那么这些字符是怎么被映射的呢?”

木森(继续解释):“这个映射是根据Base64的编码规则来的。大写字母A-Z映射到0-25,小写字母a-z映射到26-51,数字0-9映射到52-61,+映射到62,/映射到63。”

艾丽卡(点头):“原来是这样。那么,主函数base64Decode是怎么使用这个映射的呢?”

木森(耐心地):“在base64Decode函数中,首先会移除输入字符串中的任何非Base64字符,比如换行符或空格。然后,它会计算输出数组的长度,并创建一个Uint8Array数组来存储转换后的二进制数据。”

艾丽卡(好奇):“那么,它是如何将Base64字符转换为二进制数据的呢?”

木森(微笑):“这个过程是通过一系列的位操作来完成的。函数会遍历输入字符串的每个字符,使用b64ToUint6函数获取每个字符的6位数值,并将这些值组合成一个24位的整数。然后,它会将这个24位的整数拆分成三个字节,并将它们存储到输出数组中。”

艾丽卡(恍然大悟):“哦,我明白了!那么,如果输入字符串的长度不是3的倍数,它是怎么处理的呢?”

木森(点头):“在Base64编码中,如果输入数据的长度不是3的倍数,会在编码的字符串的末尾添加一个或两个=字符作为填充。在解码过程中,如果遇到这种情况,函数会忽略这些填充字符。”

艾丽卡(兴奋地):“这真是太神奇了!我们可以用这个函数来解码任何Base64编码的字符串,得到原始的二进制数据。”

看完了编译器的大致流程,让我们在回到index.js中

这段代码是一个编译函数的核心部分,它处理编译配置、解析源文件、并调用底层模块进行编译。让我们一步步地分析这个函数的工作原理,特别是它是如何处理source的。

  1. 定义解析器
const resolver = (0, exports.sourcesResolver)(compileConfig.sources);

这行代码创建了一个解析器函数,它用于从编译配置中提供的源文件信息中获取实际的源代码。这个解析器函数通常是一个高级函数,能够根据文件名或其他标识符读取源文件的内容。

  1. 确定编译目标
let targets = compileConfig.targets;
if (targets === undefined && Array.isArray(compileConfig.sources)) {
    targets = compileConfig.sources.map(s => s.filename);
}
if (targets === undefined) {
    throw new Error('`sources` is not an array and `targets` were not provided');
}

这部分代码首先尝试从编译配置中获取targets,如果未定义且sources存在,它会尝试从sources数组中提取文件名作为目标。如果两者都未定义,将抛出错误

  1. 检查源文件是否存在
const entryWithNoSource = targets.find(filename => {
    try {
        resolver(filename);
        return false;
    }
    catch (e) {
        return true;
    }
});
if (entryWithNoSource) {
    throw new Error(`The entry point \`${entryWithNoSource}\` was not provided in sources.`);
}

这里,代码检查每个目标文件是否都可以通过解析器找到。如果有任何目标文件无法找到,将抛出错误。

  1. 设置编译环境
const mod = await this.createModule();
const allocatedPointers = [];
const sourceMap = {};
const sourceOrder = [];

这部分代码初始化编译模块,准备一些辅助变量,如用于存储指针的数组、源文件映射和源文件顺序列表。

  1. 定义回调函数
const callbackPtr = mod.addFunction((_kind, _data, contents, error) => {
    // 省略具体实现...
}, 'viiii');

这里定义了一个回调函数,它将被底层模块调用来处理各种编译时的事件,如请求源文件内容。

  1. 配置和启动编译
const configStr = JSON.stringify({
    sources: targets,
    optLevel: compileConfig.optLevel || 2,
});
const configStrPointer = copyToCString(mod, configStr);
allocatedPointers.push(configStrPointer);
const resultPointer = mod._func_compile(configStrPointer, callbackPtr);
allocatedPointers.push(resultPointer);
const retJson = copyFromCString(mod, resultPointer);

这部分代码将编译配置转换为字符串,并将其传递给底层模块以启动编译过程。

  1. 清理和返回结果
allocatedPointers.forEach(ptr => mod._free(ptr));
mod.removeFunction(callbackPtr);
const snapshot = [];
for (let i = sourceOrder.length - 1; i >= 0; i--) {
    const path = sourceOrder[i];
    if (sourceMap[path].included)
        continue;
    snapshot.push({
        filename: path,
        content: sourceMap[path].content,
    });
}
const ret = JSON.parse(retJson);
return {
    ...ret,
    snapshot,
};

最后,代码清理分配的资源,构建一个包含所有源文件内容的快照,并返回编译结果。

这个函数是一个完整的编译流程实现,它处理源文件的解析、编译配置的设置、编译过程的启动和结果的处理。

我们现在返回头,来看一下艾丽卡最关心的如何编译

const sourcesResolver = (sources) => {
    if (typeof sources === 'function')
        return sources;
    if (Array.isArray(sources))
        return (0, exports.arraySourceResolver)(sources);
    return (0, exports.mapSourceResolver)(sources);
};

木森(耐心地):“艾丽卡,这段代码定义了一个名为sourcesResolver的函数,它的作用是根据提供的sources参数的不同形态,返回一个合适的解析器函数。”

艾丽卡(好奇地):“哦?那sources有哪些不同的形态呢?”

木森(解释):“sources参数可以有两种不同的形态:

  1. 函数:如果sources是一个函数,那么sourcesResolver会直接返回这个函数。这种情况下,函数应该接受一个文件路径作为参数,并返回该路径对应的源代码内容。”

艾丽卡(思考):“也就是说,如果我已经有一个函数可以读取文件内容,sourcesResolver就会使用它?”

木森(点头):“没错,这样你就可以直接利用现有的函数来解析源代码,而不需要额外的转换。”

艾丽卡(继续询问):“那第二种形态是什么呢?”

木森(微笑):“2. 数组或对象:如果sources是一个数组或对象,sourcesResolver会根据数组或对象中的信息来创建一个解析器函数。

  • 数组:如果sources是一个数组,sourcesResolver会调用arraySourceResolver函数。这个函数会根据数组中的文件信息来创建一个解析器,这个解析器可以接受文件路径作为参数,并从数组中找到对应的文件内容。”

艾丽卡(恍然大悟):“我明白了,那如果sources是一个对象呢?”

木森(详细解释):“- 对象:如果sources是一个对象,sourcesResolver会调用mapSourceResolver函数。这个函数会根据对象中的键值对来创建一个解析器,这个解析器可以接受文件路径作为参数,并从对象中找到对应的文件内容。”

艾丽卡(兴奋地):“这真是太方便了!无论我们的源代码信息是函数、数组还是对象,sourcesResolver都能帮我们处理。”

是的,所以接下来我们只要修改一下前面的逻辑,让他不用判断target和source,而是直接传入源代码就好啦!

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileFunc = exports.compilerVersion = exports.latestCompiler = exports.FuncCompiler = exports.sourcesResolver = exports.arraySourceResolver = exports.mapSourceResolver = void 0;

const path_1 = require("./path");
const utils_1 = require("./utils");
const func_js_bin_1 = require("@ton-community/func-js-bin");

// 定义 source resolver 函数
const mapSourceResolver = (map) => {
    return (path) => {
        if (path in map) {
            return map[path];
        }
        throw new Error(`Cannot find source file \`${path}\``);
    };
};
exports.mapSourceResolver = mapSourceResolver;

const arraySourceResolver = (arr) => {
    return (path) => {
        const entry = arr.find(e => e.filename === path);
        if (entry === undefined)
            throw new Error(`Cannot find source file \`${path}\``);
        return entry.content;
    };
};
exports.arraySourceResolver = arraySourceResolver;

const sourcesResolver = (sources) => {
    if (typeof sources === 'function') {
        return sources;
    }
    if (Array.isArray(sources)) {
        return exports.arraySourceResolver(sources);
    }
    return exports.mapSourceResolver(sources);
};
exports.sourcesResolver = sourcesResolver;

// 定义辅助函数
const copyToCString = (mod, str) => {
    const len = mod.lengthBytesUTF8(str) + 1;
    const ptr = mod._malloc(len);
    mod.stringToUTF8(str, ptr, len);
    return ptr;
};

const copyToCStringPtr = (mod, str, ptr) => {
    const allocated = copyToCString(mod, str);
    mod.setValue(ptr, allocated, '*');
    return allocated;
};

const copyFromCString = (mod, ptr) => {
    return mod.UTF8ToString(ptr);
};

class FuncCompiler {
    constructor() {
        this.wasmBinary = (0, utils_1.base64Decode)(func_js_bin_1.object.wasmBase64);
        this.inputFuncVersion = func_js_bin_1.object.funcVersion;
        this.module = null; // 这里应该是初始化 WebAssembly 模块的逻辑
    }

    async createModule() {
        // 这里应该是加载和初始化 WebAssembly 模块的逻辑
        // 返回模块实例
        return this.module;
    }

    async compilerVersion() {
        const mod = await this.createModule();
        const versionJsonPointer = mod._version();
        const versionJson = copyFromCString(mod, versionJsonPointer);
        mod._free(versionJsonPointer);
        return JSON.parse(versionJson);
    }

    async validateVersion() {
        const v = await this.compilerVersion();
        return v.funcVersion === this.inputFuncVersion;
    }

    async compileFunc(sourceCode) {
        const mod = await this.createModule();
        const allocatedPointers = [];
        const sourceMap = { "main.fc": sourceCode };
        const sourceOrder = ["main.fc"];

        const callbackPtr = mod.addFunction((_kind, _data, contents, error) => {
            const kind = copyFromCString(mod, _kind);
            const data = copyFromCString(mod, _data);
            if (kind === 'realpath' || kind === 'source') {
                const path = "main.fc"; // Assuming the source code is in 'main.fc'
                allocatedPointers.push(copyToCStringPtr(mod, sourceMap[path], contents));
            } else {
                allocatedPointers.push(copyToCStringPtr(mod, 'Unknown callback kind ' + kind, error));
            }
        }, 'viiii');

        const configStr = JSON.stringify({
            sources: [{ filename: "main.fc", content: sourceCode }],
            optLevel: 2,
        });
        const configStrPointer = copyToCString(mod, configStr);
        allocatedPointers.push(configStrPointer);
        const resultPointer = mod._func_compile(configStrPointer, callbackPtr);
        allocatedPointers.push(resultPointer);
        const retJson = copyFromCString(mod, resultPointer);

        // Cleanup
        allocatedPointers.forEach(ptr => mod._free(ptr));
        mod.removeFunction(callbackPtr);

        const snapshot = sourceOrder.map(path => ({
            filename: path,
            content: sourceMap[path],
        }));

        const ret = JSON.parse(retJson);
        return {
            ...ret,
            snapshot,
        };
    }
}

exports.FuncCompiler = FuncCompiler;
exports.latestCompiler = new FuncCompiler();
exports.compilerVersion = exports.latestCompiler.compilerVersion;
exports.compileFunc = exports.latestCompiler.compileFunc;

艾丽卡,激动的运行,可是仍然报错了
在这里插入图片描述
这是什么原因呢?原来是因为虽然修改了js代码,但是和它同名的ts代码并没有修改,而编码仍然要检查类型。
当你在项目中同时使用 JavaScript (JS) 和 TypeScript (TS) 时,确保两者的接口和实现保持一致是非常重要的。如果你修改了 JS 代码但没有相应地更新 TS 代码,可能会出现类型不匹配的问题,因为 TypeScript 在编译时会进行类型检查。

  • 17
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值