摘要:其實(shí)和我還沒(méi)有好好研究過(guò),之前大部分都是用開(kāi)發(fā),代碼風(fēng)格檢查都是用的,所以這次選擇的代碼檢查作為開(kāi)始,通過(guò)實(shí)踐找找感覺(jué)和興趣,之后再一點(diǎn)一點(diǎn)精進(jìn)。
這個(gè)很多人都做過(guò),文章也挺多的,我也是參考別人文章的,不過(guò)直到真正實(shí)現(xiàn)還是踩了許多坑,所以記錄下來(lái),或許對(duì)其他人有幫助。其實(shí)LLVM和Clang我還沒(méi)有好好研究過(guò),之前大部分都是用Swift開(kāi)發(fā),代碼風(fēng)格檢查都是用的Swiftlint,所以這次選擇OC的代碼檢查作為開(kāi)始,通過(guò)實(shí)踐找找感覺(jué)和興趣,之后再一點(diǎn)一點(diǎn)精進(jìn)。
下載源碼代開(kāi)終端
sudo mkdir llvm
sudo chown `whoami` llvm
cd llvm
export LLVM_HOME=`pwd`
git clone -b release_60 https://github.com/llvm-mirror/llvm.git llvm
git clone -b release_60 https://github.com/llvm-mirror/clang.git llvm/tools/clang
git clone -b release_60 https://github.com/llvm-mirror/clang-tools-extra.git llvm/tools/clang/tools/extra
git clone -b release_60 https://github.com/llvm-mirror/compiler-rt.git llvm/projects/compiler-rt
下載源碼會(huì)比較慢,另外網(wǎng)上有其他文章,有的文章比較老,版本也比較老,建議用最新的release_60。
安裝cmake如果沒(méi)有安裝cmake需要安裝一下,后面需要用。
brew update brew install cmake開(kāi)始編寫(xiě)插件
cd到llvm/llvm/tools/clang/examples
1.打開(kāi)這個(gè)目錄下的CMakeLists.txt文件,然后添加add_subdirectory(CodeChecker)
2.在當(dāng)前目錄創(chuàng)建新的文件夾CodeChecker,并cd到CodeChecker
mkdir CodeChecker
cd CodeChecker
3.在新建的CodeChecker目錄下創(chuàng)建三個(gè)文件
touch CMakeLists.txt touch CodeChecker.cpp touch CodeChecker.exports
在新創(chuàng)建的CMakeLists.txt中添加
if( NOT MSVC ) # MSVC mangles symbols differently
if( NOT LLVM_REQUIRES_RTTI )
if( NOT LLVM_REQUIRES_EH )
set(LLVM_EXPORTED_SYMBOL_FILE ${CMAKE_CURRENT_SOURCE_DIR}/CodeChecker.exports)
endif()
endif()
endif()
add_llvm_loadable_module(CodeChecker CodeChecker.cpp)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(CodeChecker ${cmake_2_8_12_PRIVATE}
clangAST
clangBasic
clangFrontend
LLVMSupport
)
endif()
在CodeChecker.cpp文件中加入
#include
#include
#include
#include
#include
#include
#include
#include
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Sema/Sema.h"
using namespace clang;
using namespace std;
namespace
{
static vector<string> split(const string &s, char delim)
{
vector<string> elems;
stringstream ss;
ss.str(s);
string item;
while (getline(ss, item, delim)) {
elems.push_back(item);
}
return elems;
}
class CodeVisitor : public RecursiveASTVisitor
{
private:
CompilerInstance &Instance;
ASTContext *Context;
public:
void setASTContext (ASTContext &context)
{
this -> Context = &context;
}
private:
/**
判斷是否為用戶源碼
@param decl 聲明
@return true 為用戶源碼,false 非用戶源碼
*/
bool isUserSourceCode (Decl *decl)
{
string filename = Instance.getSourceManager().getFilename(decl->getSourceRange().getBegin()).str();
if (filename.empty())
return false;
//非XCode中的源碼都認(rèn)為是用戶源碼
if(filename.find("/Applications/Xcode.app/") == 0)
return false;
return true;
}
/**
檢測(cè)類(lèi)名是否存在小寫(xiě)開(kāi)頭
@param decl 類(lèi)聲明
*/
void checkClassNameForLowercaseName(ObjCInterfaceDecl *decl)
{
StringRef className = decl -> getName();
//類(lèi)名稱(chēng)必須以大寫(xiě)字母開(kāi)頭
char c = className[0];
if (isLowercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Class name should not start with lowercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)類(lèi)名是否包含下劃線
@param decl 類(lèi)聲明
*/
void checkClassNameForUnderscoreInName(ObjCInterfaceDecl *decl)
{
StringRef className = decl -> getName();
//類(lèi)名不能包含下劃線
size_t underscorePos = className.find("_");
if (underscorePos != StringRef::npos)
{
//修正提示
std::string tempName = className;
std::string::iterator end_pos = std::remove(tempName.begin(), tempName.end(), "_");
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告錯(cuò)誤
DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Class name with `_` forbidden");
SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)方法名是否存在大寫(xiě)開(kāi)頭
@param decl 方法聲明
*/
void checkMethodNameForUppercaseName(ObjCMethodDecl *decl)
{
//檢查名稱(chēng)的每部分,都不允許以大寫(xiě)字母開(kāi)頭
Selector sel = decl -> getSelector();
int selectorPartCount = decl -> getNumSelectorLocs();
for (int i = 0; i < selectorPartCount; i++)
{
StringRef selName = sel.getNameForSlot(i);
char c = selName[0];
if (isUppercase(c))
{
//修正提示
std::string tempName = selName;
tempName[0] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl -> getSelectorLoc(i);
SourceLocation nameEnd = nameStart.getLocWithOffset(selName.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Selector name should not start with uppercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
}
/**
檢測(cè)方法中定義的參數(shù)名稱(chēng)是否存在大寫(xiě)開(kāi)頭
@param decl 方法聲明
*/
void checkMethodParamsNameForUppercaseName(ObjCMethodDecl *decl)
{
for (ObjCMethodDecl::param_iterator it = decl -> param_begin(); it != decl -> param_end(); it++)
{
ParmVarDecl *parmVarDecl = *it;
StringRef name = parmVarDecl -> getName();
char c = name[0];
if (isUppercase(c))
{
//修正提示
std::string tempName = name;
tempName[0] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = parmVarDecl -> getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Selector"s param name should not start with uppercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
}
/**
檢測(cè)方法實(shí)現(xiàn)是否超過(guò)500行代碼
@param decl 方法聲明
*/
void checkMethodBodyForOver500Lines(ObjCMethodDecl *decl)
{
if (decl -> hasBody())
{
//存在方法體
Stmt *methodBody = decl -> getBody();
string srcCode;
srcCode.assign(Instance.getSourceManager().getCharacterData(methodBody->getSourceRange().getBegin()),
methodBody->getSourceRange().getEnd().getRawEncoding() - methodBody->getSourceRange().getBegin().getRawEncoding() + 1);
vector<string> lines = split(srcCode, "
");
if(lines.size() > 500)
{
DiagnosticsEngine &D = Instance.getDiagnostics();
unsigned diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Single method should not have body over 500 lines");
D.Report(decl -> getSourceRange().getBegin(), diagID);
}
}
}
/**
檢測(cè)屬性名是否存在大寫(xiě)開(kāi)頭
@param decl 屬性聲明
*/
void checkPropertyNameForUppercaseName(ObjCPropertyDecl *decl)
{
bool checkUppercaseNameIndex = 0;
StringRef name = decl -> getName();
if (name.find("_") == 0)
{
//表示以下劃線開(kāi)頭
checkUppercaseNameIndex = 1;
}
//名稱(chēng)必須以小寫(xiě)字母開(kāi)頭
char c = name[checkUppercaseNameIndex];
if (isUppercase(c))
{
//修正提示
std::string tempName = name;
tempName[checkUppercaseNameIndex] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告錯(cuò)誤
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Property name should not start with uppercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)屬性名是否包含下劃線
@param decl 屬性聲明
*/
void checkPropertyNameForUnderscoreInName(ObjCPropertyDecl *decl)
{
StringRef name = decl -> getName();
if (name.size() == 1)
{
//不需要檢測(cè)
return;
}
//類(lèi)名不能包含下劃線
size_t underscorePos = name.find("_", 1);
if (underscorePos != StringRef::npos)
{
//修正提示
std::string tempName = name;
std::string::iterator end_pos = std::remove(tempName.begin() + 1, tempName.end(), "_");
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告錯(cuò)誤
DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Error, "Property name with `_` forbidden");
SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)委托屬性是否有使用weak修飾
@param decl 屬性聲明
*/
void checkDelegatePropertyForUsageWeak (ObjCPropertyDecl *decl)
{
QualType type = decl -> getType();
StringRef typeStr = type.getAsString();
//Delegate
if(typeStr.find("<") != string::npos && typeStr.find(">") != string::npos)
{
ObjCPropertyDecl::PropertyAttributeKind attrKind = decl -> getPropertyAttributes();
string typeSrcCode;
typeSrcCode.assign(Instance.getSourceManager().getCharacterData(decl -> getSourceRange().getBegin()),
decl -> getSourceRange().getEnd().getRawEncoding() - decl -> getSourceRange().getBegin().getRawEncoding());
if(!(attrKind & ObjCPropertyDecl::OBJC_PR_weak))
{
DiagnosticsEngine &diagEngine = Instance.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "Delegate should be declared as weak.");
diagEngine.Report(decl -> getLocation(), diagID);
}
}
}
/**
檢測(cè)常量名稱(chēng)是否存在小寫(xiě)開(kāi)頭
@param decl 常量聲明
*/
void checkConstantNameForLowercaseName (VarDecl *decl)
{
StringRef className = decl -> getName();
//類(lèi)名稱(chēng)必須以大寫(xiě)字母開(kāi)頭
char c = className[0];
if (isLowercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Constant name should not start with lowercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)變量名稱(chēng)是否存在大寫(xiě)開(kāi)頭
@param decl 變量聲明
*/
void checkVarNameForUppercaseName (VarDecl *decl)
{
StringRef className = decl -> getName();
//類(lèi)名稱(chēng)必須以大寫(xiě)字母開(kāi)頭
char c = className[0];
if (isUppercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//報(bào)告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Warning, "Variable name should not start with uppercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
/**
檢測(cè)變量名稱(chēng)
@param decl 變量聲明
*/
void checkVarName(VarDecl *decl)
{
if (decl -> isStaticLocal())
{
//靜態(tài)變量
if (decl -> getType().isConstant(*this -> Context))
{
//常量
checkConstantNameForLowercaseName(decl);
}
else
{
//非常量
checkVarNameForUppercaseName(decl);
}
}
else if (decl -> isLocalVarDecl())
{
//本地變量
if (decl -> getType().isConstant(*this -> Context))
{
//常量
checkConstantNameForLowercaseName(decl);
}
else
{
//非常量
checkVarNameForUppercaseName(decl);
}
}
else if (decl -> isFileVarDecl())
{
//文件定義變量
if (decl -> getType().isConstant(*this -> Context))
{
//常量
checkConstantNameForLowercaseName(decl);
}
else
{
//非常量
checkVarNameForUppercaseName(decl);
}
}
}
public:
CodeVisitor (CompilerInstance &Instance)
:Instance(Instance)
{
}
/**
觀察ObjC的類(lèi)聲明
@param declaration 聲明對(duì)象
@return 返回
*/
bool VisitObjCInterfaceDecl(ObjCInterfaceDecl *declaration)
{
if (isUserSourceCode(declaration))
{
checkClassNameForLowercaseName(declaration);
checkClassNameForUnderscoreInName(declaration);
}
return true;
}
/**
觀察類(lèi)方法聲明
@param declaration 聲明對(duì)象
@return 返回
*/
bool VisitObjCMethodDecl(ObjCMethodDecl *declaration)
{
if (isUserSourceCode(declaration))
{
checkMethodNameForUppercaseName(declaration);
checkMethodParamsNameForUppercaseName(declaration);
checkMethodBodyForOver500Lines(declaration);
}
return true;
}
/**
觀察類(lèi)屬性聲明
@param declaration 聲明對(duì)象
@return 返回
*/
bool VisitObjCPropertyDecl(ObjCPropertyDecl *declaration)
{
if (isUserSourceCode(declaration))
{
checkPropertyNameForUppercaseName(declaration);
checkPropertyNameForUnderscoreInName(declaration);
checkDelegatePropertyForUsageWeak(declaration);
}
return true;
}
/**
觀察變量聲明
@param declaration 聲明對(duì)象
@return 返回
*/
bool VisitVarDecl(VarDecl *declaration)
{
if (isUserSourceCode(declaration))
{
checkVarName(declaration);
}
return true;
}
/**
觀察枚舉常量聲明
@param declaration 聲明對(duì)象
@return 返回
*/
// bool VisitEnumConstantDecl (EnumConstantDecl *declaration)
// {
// return true;
// }
};
class CodeConsumer : public ASTConsumer
{
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
public:
CodeConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates)
: Instance(Instance), ParsedTemplates(ParsedTemplates), visitor(Instance)
{
}
bool HandleTopLevelDecl(DeclGroupRef DG) override
{
return true;
}
void HandleTranslationUnit(ASTContext& context) override
{
visitor.setASTContext(context);
visitor.TraverseDecl(context.getTranslationUnitDecl());
}
private:
CodeVisitor visitor;
};
class CodeASTAction : public PluginASTAction
{
std::set<std::string> ParsedTemplates;
protected:
std::unique_ptr CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override
{
return llvm::make_unique(CI, ParsedTemplates);
}
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override
{
// DiagnosticsEngine &D = CI.getDiagnostics();
// D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
// "My plugin Started..."));
return true;
}
};
}
static clang::FrontendPluginRegistry::Add
X("CodeChecker", "Code Checker");
使用cmake編譯源代碼
1.cd到llvm,注意不是llvm/llvm,執(zhí)行
mkdir llvm_build && cd llvm_build
cmake -G Xcode ../llvm -DCMAKE_BUILD_TYPE:STRING=MinSizeRel
2.然后會(huì)在llvm_build文件目錄中看到LLVM.xcodeproj,用xcode打開(kāi),選擇Automatically Create Schemes
3.編譯 clang,CodeChecker,libclang
4.在llvm_build/Debug/lib目錄下可以找到我們的插件,如下圖:
Xcode集成Plugin
創(chuàng)建需要加載插件的項(xiàng)目,在Build Settings欄目中的OTHER_CFLAGS添加上如下內(nèi)容:
-Xclang -load -Xclang (.dylib)動(dòng)態(tài)庫(kù)路徑 -Xclang -add-plugin -Xclang 插件名字
我把插件拷貝的桌面了,所以我的是:
-Xclang -load -Xclang /Users/roy.cao/Desktop/CodeChecker.dylib -Xclang -add-plugin -Xclang CodeChecker
然后你build項(xiàng)目,可能有unable to load plugin "/Users/roy.cao/Desktop/CodeChecker.dylib"的error,這是由于Clang插件需要對(duì)應(yīng)的Clang版本來(lái)加載,如果版本不一致會(huì)導(dǎo)致編譯錯(cuò)誤。
在Build Settings欄目中新增兩項(xiàng)用戶定義的設(shè)置,分別為CC和CXX CC對(duì)應(yīng)的是自己編譯的clang的絕對(duì)路徑,CXX對(duì)應(yīng)的是自己編譯的clang++的絕對(duì)路徑
/Users/roy.cao/llvm/llvm_build/Debug/bin/clang /Users/roy.cao/llvm/llvm_build/Debug/bin/clang++
再build,會(huì)有以下錯(cuò)誤:
在Build Settings欄目中搜索index,將Enable Index-Wihle-Building Functionality的Default改為NO.
再build可能會(huì)出現(xiàn)一大堆系統(tǒng)庫(kù)的 symbol not found 錯(cuò)誤
這個(gè)時(shí)候需要在剛剛OTHER_CFLAGS的
-Xclang -load -Xclang /Users/roy.cao/Desktop/CodeChecker.dylib -Xclang -add-plugin -Xclang CodeChecker
后面再加上-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.0.sdk
注意iPhoneSimulator12.0.sdk,每個(gè)人的可能不同,需要到目錄下看看自己的版本。 那么完整的就是下圖這樣:
再build就能做代碼風(fēng)格檢查啦,慶祝一下吧~~~~
參考文章:
LLVM & Clang 入門(mén)
使用Xcode開(kāi)發(fā)iOS語(yǔ)法檢查的Clang插件
Clang 之旅--使用 Xcode 開(kāi)發(fā) Clang 插件
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/7250.html
摘要:轉(zhuǎn)換時(shí)支持模板文件,配合強(qiáng)大的模板文件,可以自己創(chuàng)建,或者是實(shí)現(xiàn)語(yǔ)法高亮,還支持?jǐn)?shù)學(xué)公式編輯。標(biāo)簽文件允許這些項(xiàng)目能夠被一個(gè)文本編輯器或其它工具簡(jiǎn)捷迅速的定位。 原文地址 Vim作為一個(gè)強(qiáng)大的編輯器,再配合強(qiáng)大的插件,就可以稱(chēng)得上為編輯神器了。 pathogen pathogen為管理插件的插件,類(lèi)似的還有vundle。在 Pathogen 之前,安裝插件就是把插件文件放在.vim目錄...
摘要:優(yōu)化級(jí)別越高,編譯時(shí)間越長(zhǎng)啟用的。允許優(yōu)化,有兩個(gè)值不允許優(yōu)化器允許使用優(yōu)化器。規(guī)定是否單獨(dú)生成一個(gè)內(nèi)存初始化文件。使生成的代碼能夠感知命令行工具。設(shè)置一個(gè)絕對(duì)路徑的白名單,以防止關(guān)于絕對(duì)路徑的警告。 emcc(Emscripten Compiler Frontend)介紹 翻譯:云荒杯傾本文是Emscripten-WebAssembly專(zhuān)欄系列文章之一,更多文章請(qǐng)查看專(zhuān)欄。也可以去作...
摘要:優(yōu)化級(jí)別越高,編譯時(shí)間越長(zhǎng)啟用的。允許優(yōu)化,有兩個(gè)值不允許優(yōu)化器允許使用優(yōu)化器。規(guī)定是否單獨(dú)生成一個(gè)內(nèi)存初始化文件。使生成的代碼能夠感知命令行工具。設(shè)置一個(gè)絕對(duì)路徑的白名單,以防止關(guān)于絕對(duì)路徑的警告。 emcc(Emscripten Compiler Frontend)介紹 翻譯:云荒杯傾本文是Emscripten-WebAssembly專(zhuān)欄系列文章之一,更多文章請(qǐng)查看專(zhuān)欄。也可以去作...
閱讀 1416·2021-10-08 10:05
閱讀 3060·2021-09-26 10:10
閱讀 883·2019-08-30 15:55
閱讀 504·2019-08-26 11:51
閱讀 441·2019-08-23 18:10
閱讀 3849·2019-08-23 15:39
閱讀 658·2019-08-23 14:50
閱讀 767·2019-08-23 14:46