PlatON致力于建设下一代隐私计算架构和数据交换网络，基于现代密码技术和区块链技术，创造全新的计算范式，保证用户数据隐私的前提下，无需依赖第三方就可进行协同计算并验证结果的完整性。创建并运行debian10镜像docker run --name platon --restart always -itd debian:10 进入debian终端docker exec -it platon /bin/bash 换源（加速下载包）和更新/bin/bash -c 'echo -e "\ deb http://mirrors.163.com/debian/ buster main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-updates main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-backports main non-free contrib\n\ deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib" > /etc/apt/sources.list;\ apt update;\ apt upgrade -y;' 安装所需的包，编译程序和合约所需的库apt install -y git gcc golang npm libgmp-dev libssl-dev libtinfo5 procps nano wget 安装plantongit clone https://github.com/PlatONnetwork/PlatON-Go ~/PlatON-Go -b release-0.12.0 --depth 1 --recursive --shallow-submodules cd ~/PlatON-Go && make all echo 'export PATH="$HOME/PlatON-Go/build/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc 启动单节点测试网1、生成nodekey和blskey等相关文件mkdir -p ~/platon-node/data && keytool genkeypair | tee >(grep "PrivateKey" | awk '{print $2}' > ~/platon-node/data/nodekey) >(grep "PublicKey" | awk '{print $3}' > ~/platon-node/data/nodeid) && keytool genblskeypair | tee >(grep "PrivateKey" | awk '{print $2}' > ~/platon-node/data/blskey) >(grep "PublicKey" | awk '{print $3}' > ~/platon-node/data/blspub) 2、创建钱包文件，使用空白密码mkdir -p ~/platon-node/data && platon --datadir ~/platon-node/data account new 3、在目录~/platon-node下生成创世块配置文件platon.jsonnano ~/platon-node/platon.json 三个关键字段:node 只修改节点公钥部分，查看cat ~/platon-node/data/nodeidblsPubKey 节点BLS公钥，查看cat ~/platon-node/data/blspuballoc 给测试账户配置一定的余额，之前生成的在目录platon-node/data/keystore下模板：{ "config": { "chainId": 299, "eip155Block": 1, "ewasmBlock": 0, "VMInterpreter": "evm", "cbft": { "initialNodes": [{ "node": "enode://0fd7c8f228ff8549afd2a5a56cb688800b8b66706add2bc0a2190a09fee228d990e4a9f410b052e030abed9c6611f63798481992551cf3f3e4f8a736adfdc341@127.0.0.1:16789", "blsPubKey": "8b0069a77e06da5ef94dad38ac74a26eee35a77c2b4ddd9397eb0ffa7f9d9210dc0f773ede77c9e5e3c45650d3dc5c14528ed063376d3ba295b219bccd322f8abd5f2432bade68149eeb97d544b2154700ff1b0d986b17e732b6fe64de6a6488" }], "amount": 10, "period": 10000, "validatorMode": "ppos" }, "genesisVersion": 2304 }, "economicModel":{ "common":{ "maxEpochMinutes":4, "maxConsensusVals":4, "additionalCycleTime":28 }, "staking":{ "stakeThreshold": 1000000000000000000000000, "operatingThreshold": 10000000000000000000, "maxValidators": 30, "HesitateRatio":1, "unStakeFreezeDuration": 2 }, "slashing":{ "slashFractionDuplicateSign": 100, "duplicateSignReportReward": 50, "maxEvidenceAge":1, "slashBlocksReward":20, "zeroProduceCumulativeTime":3, "zeroProduceNumberThreshold":2 }, "gov": { "versionProposalVoteDurationSeconds": 160, "versionProposalSupportRate": 667, "textProposalVoteDurationSeconds": 160, "textProposalVoteRate": 5, "textProposalSupportRate": 667, "cancelProposalVoteRate": 50, "cancelProposalSupportRate": 667, "paramProposalVoteDurationSeconds": 160, "paramProposalVoteRate": 50, "paramProposalSupportRate": 667 }, "reward":{ "newBlockRate": 50, "platonFoundationYear": 10 }, "innerAcc":{ "platonFundAccount": "0x493301712671ada506ba6ca7891f436d29185821", "platonFundBalance": 0, "cdfAccount": "0xc1f330b214668beac2e6418dd651b09c759a4bf5", "cdfBalance": 331811981000000000000000000 } }, "nonce": "0x0376e56dffd12ab53bb149bda4e0cbce2b6aabe4cccc0df0b5a39e12977a2fcd23", "timestamp": "0x5bc94a8a", "extraData": "0xd782070186706c61746f6e86676f312e3131856c696e757800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "gasLimit": "4712388", "alloc": { "1000000000000000000000000000000000000003": { "balance": "200000000000000000000000000" }, "83837d7096180c8ed76b9b06770f4ffaec9b525e": { "balance": "999000000000000000000" } }, "number": "0x0", "gasUsed": "0x0", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"}注：官方文档过期了，HesitateRatio字段缺少和gov字段配置错误。4、初始化创世区块platon --datadir ~/platon-node/data init ~/platon-node/platon.json 5、启动单节点cd ~/platon-node && (nohup platon --identity "platon" --datadir ./data --port 16789 --rpcaddr 127.0.0.1 --rpcport 6789 --rpcapi "db,platon,net,web3,admin,personal" --rpc --nodiscover --nodekey ./data/nodekey --cbft.blskey ./data/blskey & > ./data/platon.log 2>&1 &) 6、查看和验证platon attach http://localhost:6789 --exec platon.blockNumber #区块高度 tail -f nohup.out #打印日志跟踪 platon attach http://localhost:6789 --exec 'personal.unlockAccount(platon.accounts[0],"")' #解锁第一个账号 合约开发工具安装1、安装solc编译器,platon暂不支持高版本的evm字节码npm i -g solc@0.5.6 2、安装platon-cpp编译器,可以将cpp文件编译成wasm字节码wget https://github.com/PlatONnetwork/PlatON-CDT/releases/download/v0.13.0/platon-cdt.tar.gz -O cdt.gz tar -xvf cdt.gz -C /usr/local/ --strip-components 1 && rm cdt.gz 3、工具检查，包括platon-cpp、wasm-opt、solcjsls -lh /usr/local/bin platon合约开发1、创建合约文件目录mkdir ~/helloworld 2、编辑evm合约hello.sol文件nano ~/helloworld/hello.sol 模板：pragma solidity >=0.5.0 <0.6.0; contract Hello { struct Message{ string head; string body; string end; } Message[] private mms; constructor(string memory a,string memory b, string memory c) public { mms.push(Message(a,b,c)); } function add_message (string memory a,string memory b, string memory c) public { mms.push(Message(a,b,c)); } function get_message_size () public view returns (uint8) { return uint8(mms.length); } function get_message_body (uint8 index) public view returns (string memory) { return mms[index].body; } } 3、编辑wasm合约hello.cpp文件nano ~/helloworld/hello.cpp 模板：#include <platon/platon.hpp> #include <string> using namespace platon; class message { public: std::string head; std::string body; std::string end; PLATON_SERIALIZE(message, (head)(body)(end)) }; CONTRACT HelloWorld : public platon::Contract{ public: ACTION void init(const message &one_message){ info.self().push_back(one_message); } ACTION void add_message(const message &one_message){ info.self().push_back(one_message); } CONST uint8_t get_message_size(){ return info.self().size(); } CONST std::string get_message_body(const uint8_t index){ return info.self()[index].body; } private: platon::StorageType<"myvector"_n, std::vector<message>> info; }; PLATON_DISPATCH(HelloWorld, (init)(add_message)(get_message_size)(get_message_body)) 4、编译合约mkdir ~/helloworld/build && cd ~/helloworld solcjs --abi --bin -o build hello.sol && platon-cpp -o build/hello.wasm hello.cpp 查看编译结果，包括4个文件，两个abi和两个字节码文件ls -lh build 5、安装js调用工具npm config set registry https://registry.npm.taobao.org && npm i lerna -g cd ~/helloworld && npm i PlatONnetwork/client-sdk-js #github巨慢，重试直到成功 6、编辑合约部署调用js文件nano ~/helloworld/evm.jsevm合约部署调用实例：const Web3 = require('web3'); const fs = require("fs"); const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:6789')); const code = "0x"+fs.readFileSync("./build/hello_sol_Hello.bin","utf-8"); var abi = JSON.parse(fs.readFileSync("./build/hello_sol_Hello.abi","utf-8")); var contract = new web3.platon.Contract(abi,{data:code}); var defaultAccount (async () => { // get and unlock default account await web3.platon.personal.getAccounts((err,accounts)=>{ if (err) { throw(err) } defaultAccount = accounts[0] console.log("default account:", defaultAccount) }) console.log("unlock default account...") await web3.platon.personal.unlockAccount(defaultAccount, "").then((ret)=>{ if (!ret) { throw("unlock account filed") } console.log("unlock success") }) // contract deploy var data = contract.deploy({arguments: ["1","2","3"]}).encodeABI() console.log("contract deploy...") await web3.platon.sendTransaction({ from:defaultAccount, data:data, gas:4100000, gaslimit:4800000 },(err)=>{ if (err) { throw(err) } }).then((receipt)=>{ console.log("contract deploy success, receipt:",receipt) contract.options.address = receipt.contractAddress }) // contract exec console.log("contract exec...") await contract.methods.add_message("5","7","8").send({ from:defaultAccount, gas:1000000 }).then((receipt)=>{ console.log("contract exec success, receipt:", receipt) }) // contract call console.log("contract call...") await contract.methods.get_message_body(1).call().then((ret)=>{ console.log("contract call success, result:", ret) }) })().catch(err => console.log(err)); nano ~/helloworld/wasm.jswasm合约部署调用实例：const Web3 = require('web3'); const fs = require("fs"); const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:6789')); const bin = fs.readFileSync("./build/hello.wasm"); var abi = JSON.parse(fs.readFileSync("./build/hello.abi.json","utf-8")); var contract = new web3.platon.Contract(abi,{vmType:1}); var defaultAccount (async () => { // get and unlock default account await web3.platon.personal.getAccounts((err,accounts)=>{ if (err) { throw(err) } defaultAccount = accounts[0] console.log("default account:", defaultAccount) }) console.log("unlock default account...") await web3.platon.personal.unlockAccount(defaultAccount, "").then((ret)=>{ if (!ret) { throw("unlock account filed") } console.log("unlock success") }) // contract deploy var data = contract.deploy({data:bin.toString("hex"), arguments: [[["1"],"2","3"]]}).encodeABI() console.log("contract deploy...") await web3.platon.sendTransaction({ from:defaultAccount, data:data, gas:4100000, gaslimit:4800000 },(err)=>{ if (err) { throw(err) } }).then((receipt)=>{ console.log("contract deploy success, receipt:",receipt) contract.options.address = receipt.contractAddress }) // contract exec console.log("contract exec...") await contract.methods.add_message([["5"],"7","8"]).send({ from:defaultAccount, gas:1000000 }).then((receipt)=>{ console.log("contract exec success, receipt:", receipt) }) // contract call console.log("contract call...") await contract.methods.get_message_body(1).call().then((ret)=>{ console.log("contract call success, result:", ret) }) })().catch(err => console.log(err)); 7、部署调用合约启动节点:cd ~/platon-node && (nohup platon --identity "platon" --datadir ./data --port 16789 --rpcaddr 127.0.0.1 --rpcport 6789 --rpcapi "db,platon,net,web3,admin,personal" --rpc --nodiscover --nodekey ./data/nodekey --cbft.blskey ./data/blskey & > ./data/platon.log 2>&1 &) 8、执行js文件调用，查看打印验证合约是否执行成功cd ~/helloworld && node evm.js && node wasm.js
vnt简称维特，采用聚合链架构，打造分布式智能价值网络。为多种资产授权、交易、流转提供低廉的资产服务，实现数字资产的高效流转。创建并运行debian10镜像docker run --name vnt --restart always -itd debian:10 进入debian终端docker exec -it vnt /bin/bash 换源（加速下载包）和更新/bin/bash -c 'echo -e "\ deb http://mirrors.163.com/debian/ buster main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-updates main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-backports main non-free contrib\n\ deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib" > /etc/apt/sources.list;\ apt update;\ apt upgrade -y;' 安装所需的包apt install -y curl git gcc npm nano 安装go1.14cd /usr/local/ && curl -O https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz tar -xvf go1.14.2.linux-amd64.tar.gz && rm go1.14.2.linux-amd64.tar.gz ln go/bin/go bin/go && ln go/bin/gofmt bin/gofmt && go env -w GOPROXY=https://goproxy.cn 安装vntgit clone https://github.com/vntchain/go-vnt ~/go/src/github.com/vntchain/go-vnt --depth 1 cd ~/go/src/github.com/vntchain/go-vnt/cmd/gvnt && go install . echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc 配置和启动一个测试节点1、生成一个见证人账户，记住账户地址和密码vntgvnt account new --datadir ~/vntnode 2、生成初始见证人的p2p地址，会打印在终端上，如：/ip4/127.0.0.1/tcp/30303/ipfs/1kHQwhd4kSVunUVPSbUH8av9FKLcrDYJDopjrwBYz9a9SDv，按ctrl+c结束gvnt --datadir ~/vntnode 3、移除临时数据gvnt目录，否则使用配置文件初始化节点时会失败rm -rf ~/vntnode/gvnt 4、使用前2步获得的账号和p2p地址创建创世块配置文件dpos.jsonnano ~/vntnode/dpos.json 修改三个字段：witnessesUrl 初始见证人p2p地址列表alloc 初始见证人余额分配witnesses 初始见证人列表模板：{"config": { "chainId": 1012, "dpos": { "period": 2, "witnessesNum": 1, "witnessesUrl": [ "/ip4/127.0.0.1/tcp/30303/ipfs/1kHQwhd4kSVunUVPSbUH8av9FKLcrDYJDopjrwBYz9a9SDv" ] } }, "timestamp": "0x5c2a3d00", "extraData": "0x", "gasLimit": "0x47b760", "difficulty": "0x1", "coinbase": "0x0000000000000000000000000000000000000000", "alloc": { "0a85b5e48a789edaa73a7c46e3396210c255df8d": { "balance": "0x200000000000000000000000000000000000000000000000000000000000000" } }, "witnesses": [ "0x0a85b5e48a789edaa73a7c46e3396210c255df8d" ], "number": "0x0", "gasUsed": "0x0", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"}4、使用dpos.json初始化和启动节点，注意开启rpc支持gvnt init ~/vntnode/dpos.json --datadir ~/vntnode gvnt --datadir ~/vntnode --rpc --rpcport 8546 --rpcapi admin,personal,rpc,core,dpos,net,vnt console 5、手动开启关闭挖矿personal.unlockAccount(core.coinbase, "", 3153600000) //中间参数是账户密码，最后一个参数指解锁的秒数 bp.start() bp.stop() 启动过程中注意见证人账户地址和密码以及开启的rpc端口，部署调用合约需要用到，在创世区块给见证人账户地址发钱用于部署合约花费wasm编译器安装与编译直接使用维特提供的docker版编译器，不能在容器shell里操作，新建一个本地shell，快捷键ctrl+shift+tdocker pull vntchain/bottle:0.6.1 docker run --rm -v <contract directory>:/tmp vntchain/bottle:0.6.1 compile -code /tmp/<contract file name> 下载erc20代码git clone https://github.com/ooozws/bottle-contract-template ~/erc20 --depth 1 编译wasm字节码，合约目录要使用绝对路径,编译结果在目录~/erc20/contracts/output下,如果要查看需要更改权限（docker容器权限问题）docker run --rm -v ~/erc20/contracts:/tmp vntchain/bottle:0.6.1 compile -code /tmp/Erc20.c sudo chmod +rx -R ~/erc20/contracts/output 复制编译的wasm到容器vnt容器docker cp ~/erc20/contracts/output vnt:/root 部署调用合约，切换到vnt容器shell，使用类似web3js的vnt.js,部署合约需要用到compress文件和abi文件安装vnt.js和进入node控制台cd ~/output && npm install --save https://github.com/vntchain/vnt.js.git && node 输入执行以下示例命令var Vnt = require("vnt") var fs = require("fs") var vnt = new Vnt() var sleep = () => new Promise((res) => setTimeout(res, 2000)) vnt.setProvider(new vnt.providers.HttpProvider("http://127.0.0.1:8546")) var acct = vnt.core.accounts[0] var abi = JSON.parse(fs.readFileSync('TokenERC20.abi', 'utf-8')) var code = '0x' + fs.readFileSync('TokenERC20.compress').toString('hex'); async function deploy() { // 使用abi初始化一个合约对象 var contract = vnt.core.contract(abi) var deployContract = contract.new( 1000000000, "bitcoin", "BTC", { from: acct, data: code, gas: 4000000 } ) await sleep() return deployContract } (async () => { vnt.personal.unlockAccount(acct, '') var contract = await deploy() console.log("contract addr:",contract.address) r = contract.GetTokenName.call({ from: acct }) console.log("TokenName", r.toString()) r = contract.GetTokenName.sendTransaction({ from: acct }) console.log("tx hash", r.toString()) })()
ontology简称本体，是新一代公有基础链项目与分布式信任协作平台，提供智能合约支持。创建并运行debian10镜像docker run --name onto --restart always -p 20334-20336:20334-20336 -itd debian:10 进入debian终端docker exec -it onto /bin/bash 换源（加速下载包）和更新/bin/bash -c 'echo -e "\ deb http://mirrors.163.com/debian/ buster main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-updates main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-backports main non-free contrib\n\ deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib" > /etc/apt/sources.list;\ apt update;\ apt upgrade -y;' 安装所需的包apt install -y curl git gcc xxd 安装go1.14，设置go代理，默认的无法访问cd /usr/local/ && curl -O https://dl.google.com/go/go1.14.4.linux-amd64.tar.gz tar -xvf go1.14.4.linux-amd64.tar.gz && rm go1.14.4.linux-amd64.tar.gz ln go/bin/go bin/go && ln go/bin/gofmt bin/gofmt && go env -w GOPROXY=https://goproxy.cn 安装ontologygit clone https://github.com/ontio/ontology ~/go/src/github.com/ontio/ontology -b v1.10.0 --depth 1 cd ~/go/src/github.com/ontio/ontology && go install . echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc 安装rustcurl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc rustup换源，加速下载/bin/bash -c 'echo -e "\ [source.crates-io]\n\ replace-with = "tuna"\n\ \n\ [source.tuna]\n\ registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"" > $HOME/.cargo/config;' 安装wasm编译器rustup target add wasm32-unknown-unknown cargo install --git=https://github.com/ontio/ontio-wasm-build 拉取rust版wasm工具和示例代码cd && git clone https://github.com/ontio/ontology-wasm-cdt-rust --depth 1 编译示例代码，编译出来的wasm文件在target/wasm32-unknown-unknown/releasecd ontology-wasm-cdt-rust && bash build.sh 生成测试钱包，需要输入密码，记住这个密码，启动节点和签名交易需要它，以123456密码为例cd && ontology account add -d 启动测试节点，使用-p参数指定密码或者按提示输入ontology --testmode -p 123456 在网站smartx.ont.io上部署wasm合约smartx.ont.io是本体在线开发合约工具，支持编辑、编译、部署等一站式服务，但是目前不支持wasm合约的编辑与编译，可以部署wasm合约，还需要安装本体的钱包来签名和发送交易。通过合约子命令部署与调用合约生成16进制的合约代码，wasm合约是二进制形式的，本地部署不支持这种形式的，使用xxd命令转换，以前面的示例合约hello为例xxd -p ~/ontology-wasm-cdt-rust/target/wasm32-unknown-unknown/release/helloworld.wasm | tr -d $'\n' > ~/hello 部署合约，如果成功将会打印合约地址ontology contract deploy --gasprice 0 --gaslimit 100000000 --vmtype 3 --code hello --name hello 调用合约，下面的地址填写实际部署成功返回的合约地址ontology contract invoke --gasprice 0 --gaslimit 100000000 --address 9fa035fbf7b10dffa1d98003c97136fa88c0323a --vmtype 3 --params string:hello 注：可以使用-p参数来预执行交易，即本地交易，不会将此交易打包到区块中去
parity是rust语言实现的以太坊客户端，现改名为openethereum，目标是成为最快、最轻和最安全的以太坊客户端。它和go版本的geth功能差不多，支持wasm虚拟机，但不自带cpu挖矿。一、环境构建创建并运行debian:10镜像docker run --name parity --restart always -p 8545 -itd debian:10 连接debian终端docker exec -it parity /bin/bash 换源（加速下载包）和更新/bin/bash -c 'echo -e "\ deb http://mirrors.163.com/debian/ buster main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-updates main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-backports main non-free contrib\n\ deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib" > /etc/apt/sources.list;\ apt update;\ apt upgrade -y;'安装所需的包apt install -y curl cmake g++ clang npm git nano 安装rustup来安装rustcurl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly 把rustup添加到环境变量path并生效echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc rustup换源，加速下载/bin/bash -c 'echo -e "\ [source.crates-io]\n\ replace-with = "tuna"\n\ \n\ [source.tuna]\n\ registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"" > $HOME/.cargo/config;' 下载openethereum代码git clone https://github.com/openethereum/openethereum.git -b v3.0.1 ~/openethereum 编译并安装openethereumcd ~/openethereum && cargo install --path . --features final 安装wasm工具，添加wasm32-unknown-unknown和安装wasm-buildrustup target add wasm32-unknown-unknown && cargo install pwasm-utils-cli --bin wasm-build 二、合约下载wasm实例项目pwasm-tutorialgit clone https://github.com/openethereum/pwasm-tutorial.git --depth 1 ~/pwasm-tutorial 编译项目中的token合约,编译成功后，可以在目录~/pwasm-tutorial/step-5/target及其子目录json下分别找到wasm和abi文件cd ~/pwasm-tutorial/step-5 && ./build.sh 启动测试节点和导入测试账户，测试账户在创世时已经分配一些余额用于合约测试openethereum --chain ~/pwasm-tutorial/wasm-dev-chain.json --jsonrpc-apis=all & curl --data '{"jsonrpc":"2.0","method":"parity_newAccountFromPhrase","params":["user", "user"],"id":0}' -H "Content-Type: application/json" -X POST localhost:8545 安装web3和进入node交互控制台cd ~/pwasm-tutorial && npm install web3 && node 部署调用流程，进入node交互控制台，参考https://github.com/openethereum/pwasm-tutorial#deploy操作var Web3 = require("web3"); var fs = require("fs"); // 连接本地节点 var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); // 设置默认账户为测试账户 web3.eth.defaultAccount = "0x004ec07d2329997267ec62b4166639513386f32e"; // 读取合约abi var abi = JSON.parse(fs.readFileSync("./step-5/target/json/TokenInterface.json")); // 读取16进制的合约代码 var codeHex = '0x' + fs.readFileSync("./step-5/target/pwasm_tutorial_contract.wasm").toString('hex'); var TokenContract = new web3.eth.Contract(abi, { data: codeHex, from: web3.eth.defaultAccount }); var TokenDeployTransaction = TokenContract.deploy({data: codeHex, arguments: [10000000]}); // 部署合约，可能提示gaslimit不够，再次执行此句部署 web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user").then(() => TokenDeployTransaction.estimateGas()).then(gas => TokenDeployTransaction.send({gasLimit: gas, from: web3.eth.defaultAccount})).then(contract => { console.log("Address of new contract: " + contract.options.address); TokenContract = contract; }).catch(err => console.log(err)); // token转账 web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user").then(() => TokenContract.methods.transfer("0x7BA4324585CB5597adC283024819254345CD7C62", 200).send()).then(console.log).catch(console.log); // 余额查询 TokenContract.methods.balanceOf("0x7BA4324585CB5597adC283024819254345CD7C62").call().then(console.log); TokenContract.methods.balanceOf(web3.eth.defaultAccount).call().then(console.log); 一键部署调用测试编辑js文件cd ~/pwasm-tutorial && nano wasm-5.js 输入以下内容保存，ctrl+o保存，ctrl+x关闭编辑器const Web3 = require('web3') const fs = require("fs") const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545')) web3.eth.defaultAccount = "0x004ec07d2329997267ec62b4166639513386f32e" var testAccount = "0x7BA4324585CB5597adC283024819254345CD7C62" var abi = require("./step-5/target/json/TokenInterface") var code = '0x' + fs.readFileSync("./step-5/target/pwasm_tutorial_contract.wasm").toString('hex') var TokenContract = new web3.eth.Contract(abi, { from: web3.eth.defaultAccount }) var TokenDeployTransaction = TokenContract.deploy({ data: code, arguments: [10000000000000] }); (async () => { await web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user") gas = await TokenDeployTransaction.estimateGas() TokenContract = await TokenDeployTransaction.send({ gasLimit: gas }) console.log("Address of new contract: " + TokenContract.options.address); await web3.eth.personal.unlockAccount(web3.eth.defaultAccount, "user") console.log(await TokenContract.methods.balanceOf(web3.eth.defaultAccount).call()) await TokenContract.methods.transfer(testAccount, 2000).send(console.log) console.log(await TokenContract.methods.balanceOf(testAccount).call()) console.log(await TokenContract.methods.balanceOf(web3.eth.defaultAccount).call()) })().catch(err => console.log(err)); 执行js文件node wasm-5.js
EOS，可以理解为Enterprise Operation System，即为商用分布式应用设计的一款区块链。EOS是一个新的高性能区块链平台，可以快速部署高性能和高安全性的区块链应用。有以下特色：支持免费交易极快出块速度（0.5s）短的区块确认（1.5s）WASM智能合约账户权限管理上下文无关交易并行验证一、EOS主网启动初始超级节点和创世节点选取、众筹活动代币冻结在EOS主网上线之前，超级节点社区将从成员里面挑选一批志愿参与上线候选池（Go-Live Pool）的技术精英。这个上线候选池中应该有大概50人。在上线之前，从候选池随机选出22个人组建一个上线小队（Go-Live team)。然后，他们开始建立高度加密互通的VPN 连接，以阻挡DDOS攻击及其他黑客攻击。EOS Token的众筹活动（以太坊ERC-20代币合约）截止在2018年的6月2日22:00，在这一刻，代币将被冻结。最后一刻会生成一个包含所有EOS Token数量的快照。快照需要被上线小队22名成员中的15人确认，随机选择22人中的1人作为创世节点，其余为初始超级节点。创世节点随机一个EOS密钥（创世密钥）。创世节点引导创建EOS区块链根据之前的包含EOS Token数量的快照和创世密钥建立EOS区块链上第一个区块（创世区块）。利用创世密钥和创世区块，创世节点启动，构建初始EOS区块链网络，自动根据创世密钥创建eosio系统账号。创世节点安装21个指定的初始超级节点，这些节点将负责进行首次超级节点投票选举。创世节点将系统的算力分配给这21个超级节点。将系统账号的权限从创世节点交给21个超级节点。接下来，创世节点将发布自己刚才使用的创世密钥（现在已无用）。超级节点选举、初始超级节点替换初始超级节点连接初始EOS区块链网络，并且对系统账户以及余额进行验证，然后开始生成区块。一旦21个指定节点都已经上线，EOS区块链网络初步启动成功，后面参与进来的人就可以相互连接了。上线小队中的剩余成员可以连接EOS区块链，并将自己置于超级节点候选人的位置上。EOS Token持币人也可以接入EOS区块链，并且用代币来换选票，用选票来进行超级节点的投票。初始超级节点的职责只有一个：组织选举，直到21个新超级节点产生。21个当选超级节点将进行几轮的区块生产来组织整个EOS主网，而上线小队中剩余的超级节点以候选者身份接入EOS主网，他们同样也可以参与EOS主网运行。就此主网已经启动就绪，已当选的21个超级节点对主网的普通交易进行处理，开始无尽的候选者选举、当选超级节点等流程。启动过程可以总结为两步：创建，配置和启动创世节点从单一超级节点过渡到21个超级节点二、账户模型相比其它的区块链简单的公私钥账户模型，EOS提供了完善的账户体系，包括自定义账户标识和权限管理等等。1、钱包钱包是一个用来存储私钥的地方使用钱包之前，需要先打开解锁钱包打开之后的钱包，有两种状态：解锁状态和锁定状态钱包，是一个超高强度加密的文件。失去钱包密码的话，意味着你丢失了钱包中的私钥2、账户类似各个APP上用户名，代表你身份的唯一标识，有以下几个特点：是一个可读的字符串，创建之后，存储在区块链中。长度为1到12个字符，字符可以包括小写字母a-z，数字1-5和点（.）（最后一个字符除外）。账户创建者自己选择命名。由排列组合得出共有 31×（32^n从0到11的和）=2^60−1=1,152,921,504,606,846,975个账户，10^18级别私钥控制，且可以更换，所以可以更换私钥来确保账户安全3、权限当一个账户创建的时候，具备了两种基本权限。每个权限绑定到一个公钥上（单签名账户）或多个公钥上（多签名账户），除了绑定公钥也可以绑定到另一个有效的账户上：owner：账号主权限, 声明的这个账号的归属。拥有这个权限的私钥需要严格保存，可以用来设置其他权限和其它所有操作。active：活动权限，顾名思议，这个权限一般用来做一些转账、投票、发起事务等常规操作。除了以上两种默认权限，还可以对账户自定义新的权限。在EOS节点上，账户权限使用数组的方式存储，每一个元素包括三个字段：权限名称、父权限名称、允许的权限，权限可以链接到其父权限，形成以owner为根的权限树，这就是在EOSIO中允许分层权限的原因。4、权限的阈值公钥的权重一个权限的许可需要公钥的权重不小于权限的阈值单签名账户：就是权限的阈值和钥匙的权重都为1的一种账户类型。使用某个权限，只需要一把对应的私钥就行了多签名账户：一个权限绑定了多个公钥，权限的阈值和每一个公钥的权重可以自定义，使用某个权限，需要多个公钥的许可且权重和不小于阈值注：这里的许可就是公钥对应的私钥签名三、资源模型EOS网络有3种资源可用，即RAM、CPU和NET，这些资源由21个超级节点决定和提供。RAM（内存）：RAM是区块链账户和智能合约消耗的重要系统资源之一。RAM充当永久性存储，用于存储帐户数据，它是有限的持久资源。CPU（中央处理器）：代表交易的处理时间，以微秒（μs）为单位。CPU是一种临时系统资源，一段时间后可恢复NET（带宽）：代表传输交易所用带宽，以字节（byte）为单位。NET也是一种临时系统资源，一段时间后可恢复RAM一般被称为内存，用于在EOS网络上存储账户相关数据（包括账户名、授权信息、合约代码、合约abi和智能合约数据）。RAM是固定资源，买多少就拥有多少，有多少就可以用多少，没有分配问题，也比较容易计量。RAM的买卖，实质上花EOS从系统账户处购买，而不是买方和卖方直接的交易。不论是购买RAM，还是卖出RAM，都是参与者与系统账户之间的交互，该过程将会收取手续费。RAM的价格是基于Bancor算法，可简单理解为市场的供需模型，如果RAM供不应求，则买入RAM时就需要锁定更多的EOS，同时，卖出RAM也能获得更多的EOS。如果一笔交易使用的内存超过限制则会被拒绝，另外，对于不再使用的RAM，可以销毁并卖出。EOS采用全内存方案，将账户信息和账户数据直接放到超级节点计算机的RAM中，加快了合约的处理速度。CPU和NET通过抵押EOS获得，属于可恢复资源。账户每发起一笔交易时，就会减掉一部分CPU和NET，当CPU与宽带资源耗尽之后，完全恢复需要24小时，这样就限制了账户一段时间内使用的资源总量。CPU和NET是按照质押的EOS比例来分配的，每个账户所能获得的资源为：系统总资源*抵押代币/总的抵押代币。虽然说CPU和NET的完全恢复周期为24h，但不代表需要等24小时才能使用，其实EOS的资源是每时每刻都在恢复的。对CPU的计量主要是交易消耗的时间，对NET的计量主要是交易的大小，EOS有21个超级节点，每个节点的性能是不一样的，同样的交易，在不同的出块节点执行，消耗的CPU时间是不一样的，在这里以出块节点为准，节点将交易打包到区块时，会开一张收据，这张收据记录交易消耗的CPU和NET，其它节点都认这个收据。对于EOS区块链，可以看做一台电脑，所有的账户都可以使用它（执行交易），但是它资源是有限的，所以要限制。内存是静态的（账户一旦写入数据就一直存在，除非数据被销毁了），所以需要花钱购买，不用了可以卖掉获得一部分钱；而处理器和带宽是动态的（现在用掉了一会儿不用就会自动恢复），可以采用抵押的方式，按照抵押的比例获得对应比例的处理器和带宽。通过买卖内存和租用处理器带宽的机制，可以让开发应用的人购买和抵押EOS提供服务，供普通用户免费使用。四、智能合约入门使用debian10搭建环境，项目是C++构建的，依赖复杂，其它Linux平台使用需要安装好依赖包，不同的系统包名可能有所不同。为了方便和主机隔离，使用docker作为演示，需要先安装docker。1、环境构建创建并运行debian:10镜像docker run --name eos --restart always -p 8888:8888 -itd debian:10 连接debian终端docker exec -it eos /bin/bash 换源（加速下载包）和更新，自带的实在是太慢了/bin/bash -c 'echo -e "\ deb http://mirrors.163.com/debian/ buster main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-updates main non-free contrib\n\ deb http://mirrors.163.com/debian/ buster-backports main non-free contrib\n\ deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib" > /etc/apt/sources.list;\ apt update;\ apt upgrade -y;'安装所需的包，还包括一些必备工具apt install -y clang cmake git g++ nano curl pkg-config libusb-1.0-0-dev libgmp-dev doxygen libssl-dev libcurl4-openssl-dev libboost-all-dev zlib1g-dev graphviz 编译安装区块链eos项目，使用2.0.6版本git clone https://github.com/EOSIO/eos.git -b v2.0.6 --depth 1 --recursive --shallow-submodules ~/eosio/eos mkdir ~/eosio/eos/build && cd ~/eosio/eos/build && cmake .. && make -j$(nproc) && make install 编译安装智能合约cdt项目，使用1.7.0版本git clone https://github.com/EOSIO/eosio.cdt.git -b v1.7.0 --depth 1 --recursive --shallow-submodules ~/eosio/eosio.cdt mkdir ~/eosio/eosio.cdt/build && cd ~/eosio/eosio.cdt/build && cmake .. && make -j$(nproc) && make install 安装检查程序会安装到目录/usr/local/bin下，主要包括nodeos、keosd、cleos、eosio-cpp等命令,使用以下命令查看它们ls -l /usr/local/bin 清理编译缓存，缓存很大rm -r ~/eosio/eos/build ~/eosio/eosio.cdt/build 删除源代码，如果编译安装成功，可以删除代码rm -r ~/eosio 2、钱包和相关秘钥生成创建钱包，保存密码到~/eosio-wallet/passwd，记住钱包密码，后面需要用来解锁钱包cleos wallet create -f ~/eosio-wallet/passwd cat ~/eosio-wallet/passwd && echo 打开钱包，查看和解锁钱包cleos wallet open cleos wallet list //带有*号为已解锁钱包 cleos wallet unlock //密码为上一步创建时得到的密码 生成开发者密钥，创建一个密钥对，私钥用来签名交易，部署合约，公钥用作身份标识cleos wallet create_key 导入系统密钥（每一个eos链都有一个相同的系统密钥，用于链的治理和共识，它是众所周知的）cleos wallet import 为了方便，使用这个私钥：5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3密钥查看，命令：cleos wallet private_keys，确保钱包打开并解锁，并输入钱包密码，可以看到创建的开发者秘钥和导入的系统秘钥password: [[ "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" ],[ "EOS7X2xejGqzG5uPVfgz85oks9G4YKDsE5W4Va7rBzyPbrRzpxHqj", "5KGvAGENQzPNjaZJ82ofdvMPZU2U6RhQA5PrSobF1v6FwHKvGfM" ] ] 3、单节点启动与测试账户生成启动钱包服务，使用ps -aux命令查看钱包服务是否运行，如果没有，使用以下命令启动服务keosd & 启动区块链节点服务，加载所有基本插件，更多参数信息查看官方文档nodeos -e -p eosio \ --plugin eosio::producer_plugin \ --plugin eosio::producer_api_plugin \ --plugin eosio::chain_api_plugin \ --plugin eosio::http_plugin \ --plugin eosio::history_plugin \ --plugin eosio::history_api_plugin \ --filter-on="*" \ --access-control-allow-origin='*' \ --contracts-console \ --http-validate-host=false \ --verbose-http-errors >> nodeos.log 2>&1 & 环境检查：tail -f nodeos.log跟踪区块生产日志，按下ctrl + c以关闭日志；cleos get info 输出节点信息；cleos get account eosio，查看eosio系统账户，初始创建的默认系统账户，可以看到，公钥是之前导入的系统秘钥的公钥# tail -f nodeos.log info 2020-06-11T07:09:29.903 nodeos producer_plugin.cpp:2093 produce_block ] Produced block 4db76dfa2ec0d5be... #5 @ 2020-06-11T07:09:30.000 signed by eosio [trxs: 0, lib: 4, confirmed: 0] info 2020-06-11T07:09:30.402 nodeos producer_plugin.cpp:2093 produce_block ] Produced block b004a1a219686344... #6 @ 2020-06-11T07:09:30.500 signed by eosio [trxs: 0, lib: 5, confirmed: 0] ^C # cleos get info { "server_version": "c6a7ec0d", "chain_id": "cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f", "head_block_num": 38, "last_irreversible_block_num": 37, "last_irreversible_block_id": "000000252e6b2af2fe7a512f21df20bfc5dd1e2ed74f69a5c64d0a9539544fbf", "head_block_id": "00000026e8d449cb0057ab3f5a17bb9b2ceb6fc8baf7e0c5b672a6b698d79816", "head_block_time": "2020-06-11T07:39:40.000", "head_block_producer": "eosio", "virtual_block_cpu_limit": 207526, "virtual_block_net_limit": 1088100, "block_cpu_limit": 199900, "block_net_limit": 1048576, "server_version_string": "v2.0.6", "fork_db_head_block_num": 38, "fork_db_head_block_id": "00000026e8d449cb0057ab3f5a17bb9b2ceb6fc8baf7e0c5b672a6b698d79816", "server_full_version_string": "v2.0.6-c6a7ec0dd816f98a6840f59dca9fed04efd9f7a5" } # cleos get account eosio created: 2018-06-01T12:00:00.000 privileged: true permissions: owner 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV active 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV memory: quota: unlimited used: 2.66 KiB 创建测试账户bob和alice，权限公钥使用之前获得的开发者公钥cleos create account eosio bob EOS7X2xejGqzG5uPVfgz85oks9G4YKDsE5W4Va7rBzyPbrRzpxHqj cleos create account eosio alice EOS7X2xejGqzG5uPVfgz85oks9G4YKDsE5W4Va7rBzyPbrRzpxHqj 如果不能创建需要先解锁钱包4、hello合约示例创建hello合约mkdir ~/hello && nano ~/hello/hello.cpp 使用下面的示例代码，使用ctrl + c和ctrl + c保存并退出#include <eosio/eosio.hpp> using namespace eosio; class contract hello : public contract { public: using contract::contract; action void hi( name user ) { print( "Hello, ", user); } }; 编译hello合约，会在~/hello目录下生成同名的wasm和abi文件cd ~/hello && eosio-cpp -abigen -o hello.wasm hello.cpp 部署hello合约首先为合约创建hello帐户，公钥使用之前获得的开发者秘钥cleos create account eosio hello EOS7X2xejGqzG5uPVfgz85oks9G4YKDsE5W4Va7rBzyPbrRzpxHqj -p eosio@active 使用set contract子命令部署合约,合约使用绝对路径cleos set contract hello /root/hello -p hello@active 调用hello合约cleos push action hello hi '["bob"]' -p bob@active 如果成功，将会输出Hello, bob# cleos push action hello hi '["bob"]' -p bob@active executed transaction: 8e862b47e6cdfb78a0b941ce450a3bfd196fd1bf63351ebc376c1b7cf3d1b989 104 bytes 1331 us # hello <= hello::hi {"user":"bob"} >> Hello, bob 5、token合约示例获取token合约，使用官方提供合约git clone https://github.com/EOSIO/eosio.contracts -b v1.9.1 --depth 1 ~/eosio.contracts 编译token合约，注意查看是否生成abi和wasm文件cd ~/eosio.contracts/contracts/eosio.token && eosio-cpp -I include -o eosio.token.wasm src/eosio.token.cpp --abigen 部署token合约先创建eosio.token账户，公钥使用之前获得的系统秘钥，可能需要先解锁钱包cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV 部署合约cleos set contract eosio.token /root/eosio.contracts/contracts/eosio.token --abi eosio.token.abi -p eosio.token@active 调用token合约创建代币，指定了代币发行者，合约所有者才能创建cleos push action eosio.token create '[ "alice", "1000000000.0000 SYS"]' -p eosio.token@active 发行代币，代币所有者才能发行cleos push action eosio.token issue '[ "alice", "100.0000 SYS", "memo" ]' -p alice@active 代币转账cleos push action eosio.token transfer '[ "alice", "bob", "25.0000 SYS", "m" ]' -p alice@active 代币余额查看与验证cleos get currency balance eosio.token bob SYS cleos get currency balance eosio.token alice SYS 最终alice和bob的SYS分别是75和25root@2554169d285c:~/eosio.contracts/contracts/eosio.token# cleos push action eosio.token issue '[ "alice", "100.0000 SYS", "memo" ]' -p alice@active executed transaction: 58214acda6e872c5003228132e0995107f9678d864bd48c9116cb8367cec8984 128 bytes 766 us # eosio.token <= eosio.token::issue {"to":"alice","quantity":"100.0000 SYS","memo":"memo"} warning: transaction executed locally, but may not be confirmed by the network yet ] root@2554169d285c:~/eosio.contracts/contracts/eosio.token# cleos push action eosio.token transfer '[ "alice", "bob", "25.0000 SYS", "m" ]' -p alice@active executed transaction: 34c9daf8587f53e778c19374b3b1dd40a106ad9dd345e5c6388c1c5819fd3a45 128 bytes 1131 us # eosio.token <= eosio.token::transfer {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"} # alice <= eosio.token::transfer {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"} # bob <= eosio.token::transfer {"from":"alice","to":"bob","quantity":"25.0000 SYS","memo":"m"} warning: transaction executed locally, but may not be confirmed by the network yet ] root@2554169d285c:~/eosio.contracts/contracts/eosio.token# cleos get currency balance eosio.token bob SYS 25.0000 SYS root@2554169d285c:~/eosio.contracts/contracts/eosio.token# cleos get currency balance eosio.token alice SYS 75.0000 SYS 6、注意事项：钱包保存在~/eosio-wallet目录中，节点数据数据保存在~/.local/share/eosio目录中，区块生成日志保存在~/nodeos.log文件中上面示例中将钱包密码保存到~/eosio-wallet/passwd文件中，钱包密码非常重要，需要妥善保存如果数据和环境弄乱了，密码忘记了，可以清除相关数据重新开始EOS的账户是需要创建的，光在钱包生成秘钥是不行的交易不能执行，可能是钱包锁定了，重新解锁即可
Go语言通过自带的一个叫CGO的工具来支持C语言函数调用，同时我们可以用Go语言导出C动态库接口给其它语言使用。一个简单的cgo程序：package main //#include <stdio.h> import "C" func main() { C.puts(C.CString("Hello, World\n")) } 通过import "C"语句启用CGO特性，同时包含C语言的<stdio.h>头文件。然后通过CGO包的C.CString函数将Go语言字符串转为C语言字符串，最后调用CGO包的C.puts函数向标准输出窗口打印转换后的C字符串。一、CGO基础要使用CGO特性，需要安装C/C++构建工具链，在macOS和Linux下是要安装GCC，在windows下是需要安装MinGW工具。同时需要保证环境变量CGO_ENABLED被设置为1，这表示CGO是被启用的状态。在本地构建时CGO_ENABLED默认是启用的，当交叉构建时CGO默认是禁止的。比如要交叉构建ARM环境运行的Go程序，需要手工设置好C/C++交叉构建的工具链，同时开启CGO_ENABLED环境变量。然后通过import "C"语句启用CGO特性。1、import "C"语句如果在Go代码中出现了import "C"语句则表示使用了CGO特性，紧跟在这行语句前面的注释是一种特殊语法，里面包含的是正常的C语言代码。当确保CGO启用的情况下，还可以在当前目录中包含C/C++对应的源文件。package main /* #include <stdio.h> void printint(int v) { printf("printint: %d\n", v); } */ import "C" func main() { v := 42 C.printint(C.int(v)) } 开头的注释中写了要调用的C函数和相关的头文件，头文件被include之后里面的所有的C语言元素都会被加入到”C”这个虚拟的包中。需要注意的是，import "C"导入语句需要单独一行，不能与其他包一同import；向C函数传递参数需要直接转化成对应C语言类型传递，如上例中C.int(v)用于将一个Go中的int类型值强制类型转换转化为C语言中的int类型值Go是强类型语言，所以cgo中传递的参数类型必须与声明的类型完全一致，而且传递前必须用”C”中的转化函数转换成对应的C类型，不能直接传入Go中类型的变量。通过虚拟的C包导入的C语言符号并不需要是大写字母开头，它们不受Go语言的导出规则约束。cgo将当前包引用的C语言符号都放到了虚拟的C包中，同时当前包依赖的其它Go语言包内部可能也通过cgo引入了相似的虚拟C包，但是不同的Go语言包引入的虚拟的C包之间的类型是不能通用的。这个约束对于要自己构造一些cgo辅助函数时有可能会造成一点的影响。2、#cgo语句在import "C"语句前的注释中可以通过#cgo语句设置编译阶段和链接阶段的相关参数。编译阶段的参数主要用于定义相关宏和指定头文件检索路径。链接阶段的参数主要是指定库文件检索路径和要链接的库文件。// #cgo CFLAGS: -DPNG_DEBUG=1 -I./include // #cgo LDFLAGS: -L/usr/local/lib -lpng // #include <png.h> import "C" 上面的代码中，CFLAGS部分，-D部分定义了宏PNG_DEBUG，值为1；-I定义了头文件包含的检索目录。LDFLAGS部分，-L指定了链接时库文件检索目录，-l指定了链接时需要链接png库。因为C/C++遗留的问题，C头文件检索目录可以是相对目录，但是库文件检索目录则需要绝对路径。在库文件的检索目录中可以通过${SRCDIR}变量表示当前包目录的绝对路径：// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo 上面的代码在链接时将被展开为：// #cgo LDFLAGS: -L/go/src/foo/libs -lfoo #cgo语句主要影响CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS几个编译器环境变量。LDFLAGS用于设置链接时的参数，除此之外的几个变量用于改变编译阶段的构建参数(CFLAGS用于针对C语言代码设置编译参数)。对于在cgo环境混合使用C和C++的用户来说，可能有三种不同的编译选项：其中CFLAGS对应C语言特有的编译选项、CXXFLAGS对应是C++特有的编译选项、CPPFLAGS则对应C和C++共有的编译选项。但是在链接阶段，C和C++的链接选项是通用的，因此这个时候已经不再有C和C++语言的区别，它们的目标文件的类型是相同的。#cgo指令还支持条件选择，当满足某个操作系统或某个CPU架构类型时后面的编译或链接选项生效。比如下面是分别针对windows和非windows下平台的编译和链接选项：// #cgo windows CFLAGS: -DX86=1 // #cgo !windows LDFLAGS: -lm 其中在windows平台下，编译前会预定义X86宏为1；在非widnows平台下，在链接阶段会要求链接math数学库。如果在不同的系统下cgo对应着不同的c代码，我们可以先使用#cgo指令定义不同的C语言的宏，然后通过宏来区分不同的代码：package main /* #cgo windows CFLAGS: -DCGO_OS_WINDOWS=1 #cgo darwin CFLAGS: -DCGO_OS_DARWIN=1 #cgo linux CFLAGS: -DCGO_OS_LINUX=1 #if defined(CGO_OS_WINDOWS) const char* os = "windows"; #elif defined(CGO_OS_DARWIN) static const char* os = "darwin"; #elif defined(CGO_OS_LINUX) static const char* os = "linux"; #else # error(unknown os) #endif */ import "C" func main() { print(C.GoString(C.os)) } 这样我们就可以用C语言中常用的技术来处理不同平台之间的差异代码。3、build tag条件编译build tag是在Go或cgo环境下的C/C++文件开头的一种特殊的注释。条件编译类似于前面通过#cgo指令针对不同平台定义的宏，只有在对应平台的宏被定义之后才会构建对应的代码。但是通过#cgo指令定义宏有个限制，它只能是基于Go语言支持的windows、darwin和linux等已经支持的操作系统。如果我们希望定义一个DEBUG标志的宏，#cgo指令就无能为力了。而Go语言提供的build tag条件编译特性则可以简单做到。比如下面的源文件只有在设置debug构建标志时才会被构建：// +build debug package main var buildMode = "debug" 可以用以下命令构建：go build -tags="debug" go build -tags="windows debug" 我们可以通过-tags命令行参数同时指定多个build标志，它们之间用空格分隔。当有多个build tag时，我们将多个标志通过逻辑操作的规则来组合使用。比如以下的构建标志表示只有在”linux/386“或”darwin平台下非cgo环境“才进行构建。// +build linux,386 darwin,!cgo 其中linux,386中linux和386用逗号链接表示AND的意思；而linux,386和darwin,!cgo之间通过空白分割来表示OR的意思。二、类型转换最初CGO是为了达到方便从Go语言函数调用C语言函数（用C语言实现Go语言声明的函数）以复用C语言资源这一目的而出现的（因为C语言还会涉及回调函数，自然也会涉及到从C语言函数调用Go语言函数（用Go语言实现C语言声明的函数））。现在，它已经演变为C语言和Go语言双向通讯的桥梁。要想利用好CGO特性，自然需要了解此二语言类型之间的转换规则。1、数值类型在Go语言中访问C语言的符号时，一般是通过虚拟的“C”包访问，比如C.int对应C语言的int类型。有些C语言的类型是由多个关键字组成，但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符，比如unsigned int不能直接通过C.unsigned int访问。因此CGO为C语言的基础数值类型都提供了相应转换规则，比如C.uint对应C语言的unsigned int。Go语言中数值类型和C语言数据类型基本上是相似的，以下是它们的对应关系表。C语言类型CGO类型Go语言类型charC.charbytesinged charC.scharint8unsigned charC.ucharuint8shortC.shortint16unsigned shortC.ushortuint16intC.intint32unsigned intC.uintuint32longC.longint32unsigned longC.ulonguint32long long intC.longlongint64unsigned long long intC.ulonglonguint64floatC.floatfloat32doubleC.doublefloat64size_tC.size_tuint虽然在C语言中int、short等类型没有明确定义内存大小，但是在CGO中它们的内存大小是确定的。在CGO中，C语言的int和long类型都是对应4个字节的内存大小，size_t类型可以当作Go语言uint无符号整数类型对待。CGO中，虽然C语言的int固定为4字节的大小，但是Go语言自己的int和uint却在32位和64位系统下分别对应4个字节和8个字节大小。如果需要在C语言中访问Go语言的int类型，可以通过GoInt类型访问，GoInt类型在CGO工具生成的_cgo_export.h头文件中定义。其实在_cgo_export.h头文件中，每个基本的Go数值类型都定义了对应的C语言类型，它们一般都是以单词Go为前缀。下面是64位环境下，_cgo_export.h头文件生成的Go数值类型的定义，其中GoInt和GoUint类型分别对应GoInt64和GoUint64：typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64; typedef GoInt64 GoInt; typedef GoUint64 GoUint; typedef float GoFloat32; typedef double GoFloat64; 除了GoInt和GoUint之外，并不推荐直接访问GoInt32、GoInt64等类型。更好的做法是通过C语言的C99标准引入的<stdint.h>头文件。为了提高C语言的可移植性，在<stdint.h>文件中，不但每个数值类型都提供了明确内存大小，而且和Go语言的类型命名更加一致。Go语言类型<stdint.h>头文件类型对比如表所示。|C语言类型 |CGO类型 |Go语言类型|int8_t |C.int8_t |int8|uint8_t |C.uint8_t |uint8|int16_t |C.int16_t |int16|uint16_t |C.uint16_t |uint16|int32_t |C.int32_t |int32|uint32_t |C.uint32_t |uint32|int64_t |C.int64_t |int64|uint64_t |C.uint64_t |uint64如果C语言的类型是由多个关键字组成，则无法通过虚拟的“C”包直接访问(比如C语言的unsigned short不能直接通过C.unsigned short访问)。但是，在<stdint.h>中通过使用C语言的typedef关键字将unsigned short重新定义为uint16_t这样一个单词的类型后，我们就可以通过C.uint16_t访问原来的unsigned short类型了。对于比较复杂的C语言类型，推荐使用typedef关键字提供一个规则的类型命名，这样更利于在CGO中访问。2、Go字符串和切片在CGO生成的_cgo_export.h头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型：typedef struct { const char *p; GoInt n; } GoString; typedef void *GoMap; typedef void *GoChan; typedef struct { void *t; void *v; } GoInterface; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; 不过需要注意的是，其中只有字符串和切片在CGO中有一定的使用价值，因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本，因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数，且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针，所以它们C语言环境并无使用的价值。在导出的C语言函数中我们可以直接使用Go字符串和切片。假设有以下两个导出函数：//export helloString func helloString(s string) {} //export helloSlice func helloSlice(s []byte) {} CGO生成的_cgo_export.h头文件会包含以下的函数声明：extern void helloString(GoString p0); extern void helloSlice(GoSlice p0); 不过需要注意的是，如果使用了GoString类型则会对_cgo_export.h头文件产生依赖，而这个头文件是动态输出的。Go1.10针对Go字符串增加了一个_GoString_预定义类型，可以降低在cgo代码中可能对_cgo_export.h头文件产生的循环依赖的风险。我们可以调整helloString函数的C语言声明为：extern void helloString(_GoString_ p0); 因为_GoString_是预定义类型，我们无法通过此类型直接访问字符串的长度和指针等信息。Go1.10同时也增加了以下两个函数用于获取字符串结构中的长度和指针信息：size_t _GoStringLen(_GoString_ s); const char *_GoStringPtr(_GoString_ s); 更严谨的做法是为C语言函数接口定义严格的头文件，然后基于稳定的头文件实现代码。3、结构体、联合、枚举类型C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中，我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。结构体的内存布局按照C语言的通用对齐规则，在32位Go语言环境C语言结构体也按照32位对齐规则，在64位Go语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体，无法在CGO中访问。结构体的简单用法如下：/* struct A { int i; float f; }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a.i) fmt.Println(a.f) } 如果结构体的成员名字中碰巧是Go语言的关键字，可以通过在成员名开头添加下划线来访问：/* struct A { int type; // type 是 Go 语言的关键字 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a._type) // _type 对应 type } 但是如果有2个成员：一个是以Go语言关键字命名，另一个刚好是以下划线和Go语言关键字命名，那么以Go语言关键字命名的成员将无法访问（被屏蔽）：/* struct A { int type; // type 是 Go 语言的关键字 float _type; // 将屏蔽CGO对 type 成员的访问 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a._type) // _type 对应 _type } C语言结构体中位字段对应的成员无法在Go语言中访问，如果需要操作位字段成员，需要通过在C语言中定义辅助函数来完成。对应零长数组的成员，无法在Go语言中直接访问数组的元素，但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。/* struct A { int size: 10; // 位字段无法访问 float arr[]; // 零长的数组也无法访问 }; */ import "C" import "fmt" func main() { var a C.struct_A fmt.Println(a.size) // 错误: 位字段无法访问 fmt.Println(a.arr) // 错误: 零长的数组也无法访问 } 在C语言中，我们无法直接访问Go语言定义的结构体类型。对于联合类型，我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型，它们会被转为对应大小的字节数组。/* #include <stdint.h> union B1 { int i; float f; }; union B2 { int8_t i8; int64_t i64; }; */ import "C" import "fmt" func main() { var b1 C.union_B1; fmt.Printf("%T\n", b1) // [4]uint8 var b2 C.union_B2; fmt.Printf("%T\n", b2) // [8]uint8 } 如果需要操作C语言的联合类型变量，一般有三种方法：第一种是在C语言中定义辅助函数；第二种是通过Go语言的"encoding/binary"手工解码成员(需要注意大端小端问题)；第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。下面展示通过unsafe包访问联合类型成员的方式：/* #include <stdint.h> union B { int i; float f; }; */ import "C" import "fmt" func main() { var b C.union_B; fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b))) fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b))) } 虽然unsafe包访问最简单、性能也最好，但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型，推荐通过在C语言中定义辅助函数的方式处理。对于枚举类型，我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。/* enum C { ONE, TWO, }; */ import "C" import "fmt" func main() { var c C.enum_C = C.TWO fmt.Println(c) fmt.Println(C.ONE) fmt.Println(C.TWO) } 在C语言中，枚举类型底层对应int类型，支持负数类型的值。我们可以通过C.ONE、C.TWO等直接访问定义的枚举值。4、数组、字符串和切片在C语言中，数组名其实对应于一个指针，指向特定类型特定长度的一段内存，但是这个指针不能被修改；当把数组名传递给一个函数时，实际上传递的是数组第一个元素的地址。为了讨论方便，我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组，字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。在Go语言中，数组是一种值类型，而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。CGO的C虚拟包提供了以下一组函数，用于Go语言和C语言之间数组和字符串的双向转换：// Go string to C string // The C string is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CString(string) *C.char // Go []byte slice to C array // The C array is allocated in the C heap using malloc. // It is the caller's responsibility to arrange for it to be // freed, such as by calling C.free (be sure to include stdlib.h // if C.free is needed). func C.CBytes([]byte) unsafe.Pointer // C string to Go string func C.GoString(*C.char) string // C data with explicit length to Go string func C.GoStringN(*C.char, C.int) string // C data with explicit length to Go []byte func C.GoBytes(unsafe.Pointer, C.int) []byte 其中C.CString针对输入的Go字符串，克隆一个C语言格式的字符串；返回的字符串由C语言的malloc函数分配，不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似，用于从输入的Go语言字节切片克隆一个C语言版本的字节数组，同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组，克隆一个Go语言字节切片。该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时，克隆的内存由C语言的malloc函数分配，最终可以通过free函数释放。当C语言字符串或数组向Go语言转换时，克隆的内存由Go语言分配管理。通过该组转换函数，转换前和转换后的内存依然在各自的语言环境中，它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单，缺点是克隆需要分配新的内存和复制操作都会导致额外的开销。在reflect包中有字符串和切片的定义：type StringHeader struct { Data uintptr Len int } type SliceHeader struct { Data uintptr Len int Cap int } 如果不希望单独分配内存，可以在Go语言中直接访问C语言的内存空间：/* static char arr[10]; static char *s = "Hello"; */ import "C" import "fmt" func main() { // 通过 reflect.SliceHeader 转换 var arr0 []byte var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0)) arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0])) arr0Hdr.Len = 10 arr0Hdr.Cap = 10 // 通过切片语法转换 arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10] var s0 string var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0)) s0Hdr.Data = uintptr(unsafe.Pointer(C.s)) s0Hdr.Len = int(C.strlen(C.s)) sLen := int(C.strlen(C.s)) s1 := string((*[31]byte)(unsafe.Pointer(&C.s[0]))[:sLen:sLen]) } 因为Go语言的字符串是只读的，用户需要自己保证Go字符串在使用期间，底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。在CGO中，会为字符串和切片生成和上面结构对应的C语言版本的结构体：typedef struct { const char *p; GoInt n; } GoString; typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; 在C语言中可以通过GoString和GoSlice来访问Go语言的字符串和切片。如果是Go语言中数组类型，可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理，那么在C语言中不能长时间保存Go内存对象。5、指针间的转换在C语言中，不同类型的指针是可以显式或隐式转换的，如果是隐式只是会在编译时给出一些警告信息。但是Go语言对于不同类型的转换非常严格，任何C语言中可能出现的警告信息在Go语言中都可能是错误！指针是C语言的灵魂，指针间的自由转换也是cgo代码中经常要解决的第一个重要的问题。在Go语言中两个指针的类型完全一致则不需要转换可以直接通用。如果一个指针类型是用type命令在另一个指针类型基础之上构建的，换言之两个指针底层是相同完全结构的指针，那么我我们可以通过直接强制转换语法进行指针间的转换。但是cgo经常要面对的是2个完全不同类型的指针间的转换，原则上这种操作在纯Go语言代码是严格禁止的。cgo存在的一个目的就是打破Go语言的禁止，恢复C语言应有的指针的自由转换和指针运算。以下代码演示了如何将X类型的指针转化为Y类型的指针：var p *X var q *Y q = (*Y)(unsafe.Pointer(p)) // *X => *Y p = (*X)(unsafe.Pointer(q)) // *Y => *X 为了实现X类型指针到Y类型指针的转换，我们需要借助unsafe.Pointer作为中间桥接类型实现不同类型指针之间的转换。unsafe.Pointer指针类型类似C语言中的void*类型的指针。下面是指针间的转换流程的示意图：任何类型的指针都可以通过强制转换为unsafe.Pointer指针类型去掉原有的类型信息，然后再重新赋予新的指针类型而达到指针间的转换的目的。6、数值和指针的转换不同类型指针间的转换看似复杂，但是在cgo中已经算是比较简单的了。在C语言中经常遇到用普通数值表示指针的场景，也就是说如何实现数值和指针的转换也是cgo需要面对的一个问题为了严格控制指针的使用，Go语言禁止将数值类型直接转为指针类型！不过，Go语言针对unsafe.Pointr指针类型特别定义了一个uintptr类型。我们可以uintptr为中介，实现数值类型到unsafe.Pointr指针类型到转换。再结合前面提到的方法，就可以实现数值和指针的转换了。下面流程图演示了如何实现int32类型到C语言的char*字符串指针类型的相互转换：转换分为几个阶段：首先是int32到uintptr类型，然后是uintptr到unsafe.Pointr指针类型，最后是unsafe.Pointr指针类型到*C.char类型。7、切片间的转换在C语言中数组也一种指针，因此两个不同类型数组之间的转换和指针间转换基本类似。但是在Go语言中，数组或数组对应的切片都不再是指针类型，因此我们也就无法直接实现不同类型的切片之间的转换。不过Go语言的reflect包提供了切片类型的底层结构，再结合前面讨论到不同类型之间的指针转换技术就可以实现[]X和[]Y类型的切片转换：var p []X var q []Y pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p)) qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q)) pHdr.Data = qHdr.Data pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0]) pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0]) 不同切片类型之间转换的思路是先构造一个空的目标切片，然后用原有的切片底层数据填充目标切片。如果X和Y类型的大小不同，需要重新设置Len和Cap属性。需要注意的是，如果X或Y是空类型，上述代码中可能导致除0错误，实际代码需要根据情况酌情处理。下面演示了切片间的转换的具体流程：三、函数调用函数是C语言编程的核心，通过CGO技术我们不仅仅可以在Go语言中调用C语言函数，也可以将Go语言函数导出为C语言函数。1、Go调用C函数对于一个启用CGO特性的程序，CGO会构造一个虚拟的C包。通过这个虚拟的C包可以调用C语言函数。/* static int add(int a, int b) { return a+b; } */ import "C" func main() { C.add(1, 1) } 以上的CGO代码首先定义了一个当前文件内可见的add函数，然后通过C.add。2、C函数的返回值对于有返回值的C函数，我们可以正常获取返回值。/* static int div(int a, int b) { return a/b; } */ import "C" import "fmt" func main() { v := C.div(6, 3) fmt.Println(v) } 上面的div函数实现了一个整数除法的运算，然后通过返回值返回除法的结果。不过对于除数为0的情形并没有做特殊处理。如果希望在除数为0的时候返回一个错误，其他时候返回正常的结果。因为C语言不支持返回多个结果，因此<errno.h>标准库提供了一个errno宏用于返回错误状态。我们可以近似地将errno看成一个线程安全的全局变量，可以用于记录最近一次错误的状态码。改进后的div函数实现如下：#include <errno.h> int div(int a, int b) { if(b == 0) { errno = EINVAL; return 0; } return a/b; } CGO也针对<errno.h>标准库的errno宏做的特殊支持：在CGO调用C函数时如果有两个返回值，那么第二个返回值将对应errno错误状态。/* #include <errno.h> static int div(int a, int b) { if(b == 0) { errno = EINVAL; return 0; } return a/b; } */ import "C" import "fmt" func main() { v0, err0 := C.div(2, 1) fmt.Println(v0, err0) v1, err1 := C.div(1, 0) fmt.Println(v1, err1) } 运行这个代码将会产生以下输出：2 <nil> 0 invalid argument 我们可以近似地将div函数看作为以下类型的函数：func C.div(a, b C.int) (C.int, [error]) 第二个返回值是可忽略的error接口类型，底层对应syscall.Errno错误类型。3、void函数的返回值C语言函数还有一种没有返回值类型的函数，用void表示返回值类型。一般情况下，我们无法获取void类型函数的返回值，因为没有返回值可以获取。前面的例子中提到，cgo对errno做了特殊处理，可以通过第二个返回值来获取C语言的错误状态。对于void类型函数，这个特性依然有效。以下的代码是获取没有返回值函数的错误状态码：//static void noreturn() {} import "C" import "fmt" func main() { _, err := C.noreturn() fmt.Println(err) } 此时，我们忽略了第一个返回值，只获取第二个返回值对应的错误码。我们也可以尝试获取第一个返回值，它对应的是C语言的void对应的Go语言类型：//static void noreturn() {} import "C" import "fmt" func main() { v, _ := C.noreturn() fmt.Printf("%#v", v) } 运行这个代码将会产生以下输出：main._Ctype_void{} 我们可以看出C语言的void类型对应的是当前的main包中的_Ctype_void类型。其实也将C语言的noreturn函数看作是返回_Ctype_void类型的函数，这样就可以直接获取void类型函数的返回值：//static void noreturn() {} import "C" import "fmt" func main() { fmt.Println(C.noreturn()) } 运行这个代码将会产生以下输出：[] 其实在CGO生成的代码中，_Ctype_void类型对应一个0长的数组类型[0]byte，因此fmt.Println输出的是一个表示空数值的方括弧。以上有效特性虽然看似有些无聊，但是通过这些例子我们可以精确掌握CGO代码的边界，可以从更深层次的设计的角度来思考产生这些奇怪特性的原因。4、C调用Go导出函数CGO还有一个强大的特性：将Go函数导出为C语言函数，通过CGO的//export指令将Go语言实现的函数导出为C语言函数。这样的话我们可以定义好C语言接口，然后通过Go语言实现。下面是用Go语言实现的add函数：import "C" //export add func add(a, b C.int) C.int { return a+b } add函数名以小写字母开头，对于Go语言来说是包内的私有函数。但是从C语言角度来看，导出的add函数是一个可全局访问的C语言函数。如果在两个不同的Go语言包内，都存在一个同名的要导出为C语言函数的add函数，那么在最终的链接阶段将会出现符号重名的问题。CGO生成的_cgo_export.h文件回包含导出后的C语言函数的声明。我们可以在纯C源文件中包含_cgo_export.h文件来引用导出的add函数。如果希望在当前的CGO文件中马上使用导出的C语言add函数，则无法引用_cgo_export.h文件。因为_cgo_export.h文件的生成需要依赖当前文件可以正常构建，而如果当前文件内部循环依赖还未生成的_cgo_export.h文件将会导致cgo命令错误。#include "_cgo_export.h" void foo() { add(1, 1); } 当导出C语言接口时，需要保证函数的参数和返回值类型都是C语言友好的类型，同时返回值不得直接或间接包含Go语言内存空间的指针。四、封装qsortqsort快速排序函数是C语言的高阶函数，支持用于自定义排序比较函数，可以对任意类型的数组进行排序。尝试基于C语言的qsort函数封装一个Go语言版本的qsort函数。1、认识qsort函数qsort快速排序函数有<stdlib.h>标准库提供，函数的声明如下：void qsort( void* base, size_t num, size_t size, int (*cmp)(const void*, const void*) ); 其中base参数是要排序数组的首个元素的地址，num是数组中元素的个数，size是数组中每个元素的大小。最关键是cmp比较函数，用于对数组中任意两个元素进行排序。cmp排序函数的两个指针参数分别是要比较的两个元素的地址，如果第一个参数对应元素大于第二个参数对应的元素将返回结果大于0，如果两个元素相等则返回0，如果第一个元素小于第二个元素则返回结果小于0。下面的例子是用C语言的qsort对一个int类型的数组进行排序：#include <stdio.h> #include <stdlib.h> #define DIM(x) (sizeof(x)/sizeof((x)[0])) static int cmp(const void* a, const void* b) { const int* pa = (int*)a; const int* pb = (int*)b; return *pa - *pb; } int main() { int values[] = { 42, 8, 109, 97, 23, 25 }; int i; qsort(values, DIM(values), sizeof(values[0]), cmp); for(i = 0; i < DIM(values); i++) { printf ("%d ",values[i]); } return 0; } 其中DIM(values)宏用于计算数组元素的个数，sizeof(values[0])用于计算数组元素的大小。 cmp是用于排序时比较两个元素大小的回调函数。为了避免对全局名字空间的污染，我们将cmp回调函数定义为仅当前文件内可访问的静态函数。2、将qsort函数从Go包导出为了方便Go语言的非CGO用户使用qsort函数，我们需要将C语言的qsort函数包装为一个外部可以访问的Go函数。用Go语言将qsort函数重新包装为qsort.Sort函数：package qsort //typedef int (*qsort_cmp_func_t)(const void* a, const void* b); import "C" import "unsafe" func Sort( base unsafe.Pointer, num, size C.size_t, cmp C.qsort_cmp_func_t, ) { C.qsort(base, num, size, cmp) } 因为CGO不好直接表达C语言的函数类型，因此在C语言空间将比较函数类型重新定义为一个qsort_cmp_func_t类型。虽然Sort函数已经导出了，但是对于qsort包之外的用户依然不能直接使用该函数——Sort函数的参数还包含了虚拟的C包提供的类型。在CGO的内部机制一节中我们已经提过，虚拟的C包下的任何名称其实都会被映射为包内的私有名字。比如C.size_t会被展开为_Ctype_size_t，C.qsort_cmp_func_t类型会被展开为_Ctype_qsort_cmp_func_t。被CGO处理后的Sort函数的类型如下：func Sort( base unsafe.Pointer, num, size _Ctype_size_t, cmp _Ctype_qsort_cmp_func_t, ) 这样将会导致包外部用于无法构造_Ctype_size_t和_Ctype_qsort_cmp_func_t类型的参数而无法使用Sort函数。因此，导出的Sort函数的参数和返回值要避免对虚拟C包的依赖。重新调整Sort函数的参数类型和实现如下：/* #include <stdlib.h> typedef int (*qsort_cmp_func_t)(const void* a, const void* b); */ import "C" import "unsafe" type CompareFunc C.qsort_cmp_func_t func Sort(base unsafe.Pointer, num, size int, cmp CompareFunc) { C.qsort(base, C.size_t(num), C.size_t(size), C.qsort_cmp_func_t(cmp)) } 我们将虚拟C包中的类型通过Go语言类型代替，在内部调用C函数时重新转型为C函数需要的类型。因此外部用户将不再依赖qsort包内的虚拟C包。以下代码展示的Sort函数的使用方式：package main //extern int go_qsort_compare(void* a, void* b); import "C" import ( "fmt" "unsafe" qsort "." ) //export go_qsort_compare func go_qsort_compare(a, b unsafe.Pointer) C.int { pa, pb := (*C.int)(a), (*C.int)(b) return C.int(*pa - *pb) } func main() { values := []int32{42, 9, 101, 95, 27, 25} qsort.Sort(unsafe.Pointer(&values[0]), len(values), int(unsafe.Sizeof(values[0])), qsort.CompareFunc(C.go_qsort_compare), ) fmt.Println(values) } 为了使用Sort函数，我们需要将Go语言的切片取首地址、元素个数、元素大小等信息作为调用参数，同时还需要提供一个C语言规格的比较函数。 其中go_qsort_compare是用Go语言实现的，并导出到C语言空间的函数，用于qsort排序时的比较函数。目前已经实现了对C语言的qsort初步包装，并且可以通过包的方式被其它用户使用。但是qsort.Sort函数已经有很多不便使用之处：用户要提供C语言的比较函数，这对许多Go语言用户是一个挑战。需要改进qsort函数的包装函数，尝试通过闭包函数代替C语言的比较函数。消除用户对CGO代码的直接依赖。3、改进：闭包函数作为比较函数在改进之前我们先回顾下Go语言sort包自带的排序函数的接口：func Slice(slice interface{}, less func(i, j int) bool) 标准库的sort.Slice因为支持通过闭包函数指定比较函数，对切片的排序非常简单：import "sort" func main() { values := []int32{42, 9, 101, 95, 27, 25} sort.Slice(values, func(i, j int) bool { return values[i] < values[j] }) fmt.Println(values) } 我们也尝试将C语言的qsort函数包装为以下格式的Go语言函数：package qsort func Sort(base unsafe.Pointer, num, size int, cmp func(a, b unsafe.Pointer) int) 闭包函数无法导出为C语言函数，因此无法直接将闭包函数传入C语言的qsort函数。为此我们可以用Go构造一个可以导出为C语言的代理函数，然后通过一个全局变量临时保存当前的闭包比较函数。var go_qsort_compare_info struct { fn func(a, b unsafe.Pointer) int sync.Mutex } //export _cgo_qsort_compare func _cgo_qsort_compare(a, b unsafe.Pointer) C.int { return C.int(go_qsort_compare_info.fn(a, b)) } 其中导出的C语言函数_cgo_qsort_compare是公用的qsort比较函数，内部通过go_qsort_compare_info.fn来调用当前的闭包比较函数。新的Sort包装函数实现如下：/* #include <stdlib.h> typedef int (*qsort_cmp_func_t)(const void* a, const void* b); extern int _cgo_qsort_compare(void* a, void* b); */ import "C" func Sort(base unsafe.Pointer, num, size int, cmp func(a, b unsafe.Pointer) int) { go_qsort_compare_info.Lock() defer go_qsort_compare_info.Unlock() go_qsort_compare_info.fn = cmp C.qsort(base, C.size_t(num), C.size_t(size), C.qsort_cmp_func_t(C._cgo_qsort_compare), ) } 每次排序前，对全局的go_qsort_compare_info变量加锁，同时将当前的闭包函数保存到全局变量，然后调用C语言的qsort函数。基于新包装的函数，我们可以简化之前的排序代码：func main() { values := []int32{42, 9, 101, 95, 27, 25} qsort.Sort(unsafe.Pointer(&values[0]), len(values), int(unsafe.Sizeof(values[0])), func(a, b unsafe.Pointer) int { pa, pb := (*int32)(a), (*int32)(b) return int(*pa - *pb) }, ) fmt.Println(values) } 现在排序不再需要通过CGO实现C语言版本的比较函数了，可以传入Go语言闭包函数作为比较函数。 但是导入的排序函数依然依赖unsafe包，这是违背Go语言编程习惯的。4、改进：消除用户对unsafe包的依赖前一个版本的qsort.Sort包装函数已经比最初的C语言版本的qsort易用很多，但是依然保留了很多C语言底层数据结构的细节。 现在我们将继续改进包装函数，尝试消除对unsafe包的依赖，并实现一个类似标准库中sort.Slice的排序函数。新的包装函数声明如下：package qsort func Slice(slice interface{}, less func(a, b int) bool) 首先，我们将slice作为接口类型参数传入，这样可以适配不同的切片类型。然后切片的首个元素的地址、元素个数和元素大小可以通过reflect反射包从切片中获取。为了保存必要的排序上下文信息，我们需要在全局包变量增加要排序数组的地址、元素个数和元素大小等信息，比较函数改为less：var go_qsort_compare_info struct { base unsafe.Pointer elemnum int elemsize int less func(a, b int) bool sync.Mutex } 同样比较函数需要根据元素指针、排序数组的开始地址和元素的大小计算出元素对应数组的索引下标， 然后根据less函数的比较结果返回qsort函数需要格式的比较结果。//export _cgo_qsort_compare func _cgo_qsort_compare(a, b unsafe.Pointer) C.int { var ( // array memory is locked base = uintptr(go_qsort_compare_info.base) elemsize = uintptr(go_qsort_compare_info.elemsize) ) i := int((uintptr(a) - base) / elemsize) j := int((uintptr(b) - base) / elemsize) switch { case go_qsort_compare_info.less(i, j): // v[i] < v[j] return -1 case go_qsort_compare_info.less(j, i): // v[i] > v[j] return +1 default: return 0 } } 新的Slice函数的实现如下：func Slice(slice interface{}, less func(a, b int) bool) { sv := reflect.ValueOf(slice) if sv.Kind() != reflect.Slice { panic(fmt.Sprintf("qsort called with non-slice value of type %T", slice)) } if sv.Len() == 0 { return } go_qsort_compare_info.Lock() defer go_qsort_compare_info.Unlock() defer func() { go_qsort_compare_info.base = nil go_qsort_compare_info.elemnum = 0 go_qsort_compare_info.elemsize = 0 go_qsort_compare_info.less = nil }() // baseMem = unsafe.Pointer(sv.Index(0).Addr().Pointer()) // baseMem maybe moved, so must saved after call C.fn go_qsort_compare_info.base = unsafe.Pointer(sv.Index(0).Addr().Pointer()) go_qsort_compare_info.elemnum = sv.Len() go_qsort_compare_info.elemsize = int(sv.Type().Elem().Size()) go_qsort_compare_info.less = less C.qsort( go_qsort_compare_info.base, C.size_t(go_qsort_compare_info.elemnum), C.size_t(go_qsort_compare_info.elemsize), C.qsort_cmp_func_t(C._cgo_qsort_compare), ) } 首先需要判断传入的接口类型必须是切片类型。然后通过反射获取qsort函数需要的切片信息，并调用C语言的qsort函数。基于新包装的函数我们可以采用和标准库相似的方式排序切片：import ( "fmt" qsort "." ) func main() { values := []int64{42, 9, 101, 95, 27, 25} qsort.Slice(values, func(i, j int) bool { return values[i] < values[j] }) fmt.Println(values) } 为了避免在排序过程中，排序数组的上下文信息go_qsort_compare_info被修改，我们进行了全局加锁。因此目前版本的qsort.Slice函数是无法并发执行的。