- 图形学
- 图像处理
【免费下载链接】skia
Skia is a complete 2D graphic library for drawing Text, Geometries, and Images.
本文基于 Skia 官方贡献文档 Coding Style Guidelines,系统梳理 Skia 代码库的 C++ 编码风格约定:从文件组织、命名规则、宏与大括号风格,到类声明、onMethodName虚函数模式与参数传递惯例,并逐条对照仓库根目录 .clang-format 中的机器可执行配置,说明这些约定如何被格式化工具自动强制,帮助贡献者在提交补丁前一次性对齐项目风格。
约定沿革与适用范围
原文档开头说明:这些约定是随项目演进而逐渐成型的,早期代码并不严格遵循全部规则,但随代码演进,期望存量代码逐步向规范靠拢。因此阅读代码时遇到风格不一致的旧代码属于正常现象,新增与修改的代码应以现行规范为准。
这一规范适用于 C++ 源码与头文件,Python 工具脚本则单独遵循 Google Python Style Guide(见文末说明)。对于补丁提交流程本身(CLA、AUTHORS 文件登记、提交前自查等前置要求),可参考 Contributing to Skia 一文。
文件组织与头文件约定
扩展名与目录布局
- C++ 源文件与头文件统一使用
.cpp和.h扩展名; - 不对外暴露的私有头文件应放在
src目录下,使其不进入客户端(client)的头文件搜索路径; - 若私有头文件需要被公开头文件包含,则应放入 include/private 目录。
最小化 include 与字母序排列
规范强调“尽可能减少 include”:如果头文件中只需前向声明(forward declare)某个名称就足够,那么前向声明优先于完整 include。前向声明与文件 include 均应按字母顺序排列。
仓库根目录的 .clang-format 通过以下配置从机器层面落实了字母序要求(见第 100 行附近):
SortIncludes: true IncludeCategories: - Regex: '^<.*\.h>' Priority: 1 - Regex: '^<.*' Priority: 2 - Regex: '.*' Priority: 3 IncludeIsMainRegex: '(-_)?$'即系统头<...>中的 C 头文件、其余系统头、项目内"..."头文件被分为三个优先级类别排序,且以test/unittest结尾的文件会优先匹配对应的同名头文件。
禁止在 SkTypes.h 之前使用条件编译
不要在包含"SkTypes.h"(直接或间接)之前使用#if/#ifdef。原因是:大多数你想做条件编译判断的宏,往往要等到SkTypes.h被处理后才真正确定。这是 Skia 特有的构建顺序约束——SkTypes.h是配置宏(平台、后端能力等)的最终裁决点。
空白与换行
- 缩进使用 4 个空格,禁止 Tab;
- 使用 Unix 风格换行符(LF);
- 尽量不留行尾空白,但执行上不严格;
- 行宽控制在 100 列以内,除非换行会“丑得离谱”(可自行判断)。
以上两条硬规则对应 .clang-format 的IndentWidth: 4、UseTab: Never、TabWidth: 4与ColumnLimit: 100。
命名规范
这是整份文档篇幅最大的部分,规则可以归纳为一张“前缀表”:
| 对象 | 命名规则 | 示例 |
|---|---|---|
| 对外可见类型/函数 | Sk前缀(GPU 后端 Ganesh 代码用Gr前缀) | SkCanvas、GrContext |
| 嵌套类型 | 无需前缀 | HelperClass |
| 含方法的类/结构体/联合体数据成员 | 小写f开头 + 驼峰 | fMilesDriven |
| 纯数据访问类型 | 可不加f前缀 | milesDriven |
| 全局变量 | 小写g开头 + 驼峰 | gLoggingEnabled |
| 局部变量与参数 | 小写开头驼峰 | numCats |
constexpr/const且值程序期内固定 | k开头 + 驼峰 | kPictureSize |
| 枚举值 | k前缀;无作用域枚举追加_枚举名后缀 | kGlazed_DonutType |
| 宏 | 全大写下划线分隔;跨文件作用域的宏加SK或GR前缀 | SK_...、GR_GL_... |
| 实现文件内 static 非类函数 | 小写下划线分隔(snake_case) | tastes_like_chicken |
| extern 函数 / static 类函数 | 大写开头驼峰 | SkIsOdd |
类前缀与嵌套类型
对外可见的类型和函数使用Sk前缀表明其属于 Skia;Ganesh(Skia 的 GPU 后端,源码主要位于src/gpu目录,Gr前缀符号集中于此)中的代码使用Gr前缀。嵌套类型无需前缀:
class SkClass { public: class HelperClass { ... }; };数据成员:f 前缀
含方法的 struct/class/union 中的数据字段以小写f开头再接驼峰命名,用以和其他变量区分;而主要面向“直接字段访问”的类型则不需要f装饰:
struct GrCar { float milesDriven; Color color; }; class GrMotorcyle { public: float getMilesDriven() const { return fMilesDriven; } void setMilesDriven(float milesDriven) { fMilesDriven = milesDriven; } Color getColor() const { return fColor; } private: float fMilesDriven; Color fColor; };注意对照:GrCar是纯数据载体,字段不加f;GrMotorcyle有访问器方法,字段加f。
全局变量与局部变量
全局变量同样遵循驼峰约定,仅前缀改为g:
bool gLoggingEnabled;仓库中可见真实用例:tools/flags/CommonFlagsConfig.cpp 中的文件作用域静态数组即gPredefinedConfigs,与上述g前缀约定一致。
局部变量与函数参数为小写开头驼峰:
int herdCats(const Array& cats) { int numCats = cats.count(); }常量:k 前缀
声明为constexpr或const、且取值在程序运行期间固定的变量,以k开头再接驼峰:
int drawPicture() { constexpr SkISize kPictureSize = {100, 100}; constexpr float kZoom = 1.0f; }枚举命名
枚举值同样以k为前缀,具体形态分四种情况:
1.enum class(作用域枚举)——值无需后缀:
// Enum class does not need suffixes. enum class SkPancakeType { kBlueberry, kPlain, kChocolateChip, };2. 无作用域枚举(独占值)——k值_枚举名后缀,枚举名用单数:
// Enum should have a suffix after the enum name. enum SkDonutType { kGlazed_DonutType, kSprinkles_DonutType, kChocolate_DonutType, kMaple_DonutType, kLast_DonutType = kMaple_DonutType }; static const SkDonutType kDonutTypeCount = kLast_DonutType + 1;规则细节:枚举本体对“独占值”用单数名、对“位域”用复数名;枚举数量若需要,命名为k<单数枚举名>Count且不作为枚举成员(如上例的kDonutTypeCount),或者在枚举内保留一个kLast成员也可以。
3. 无作用域位域枚举——值以Bit结尾:
enum SkSausageIngredientBits { kFennel_SausageIngredientBit = 0x1, kBeef_SausageIngredientBit = 0x2 };4. 标志位枚举(Flags)——值以Flag结尾:
enum SkMatrixFlags { kTranslate_MatrixFlag = 0x1, kRotate_MatrixFlag = 0x2 };函数命名
实现文件内的 static 非类函数用小写下划线分隔:
static inline bool tastes_like_chicken(Food food) { return kIceCream_Food != food; }extern 函数与 static 类函数用大写开头驼峰:
bool SkIsOdd(int n); class SkFoo { public: static int FooInstanceCount(); // Not static. int barBaz(); };宏命名
宏一律全大写下划线分隔;作用域大于文件的宏应以SK或GR开头:
#define GR_GL_TEXTURE0 0xdeadbeefGanesh 中专门与 GL 相关的宏前缀为GR_GL。另外 Ganesh 倾向让宏始终有定义,用#if MACRO而非#ifdef MACRO:
#define GR_GO_SLOWER 0 ... #if GR_GO_SLOWER Sleep(1000); #endifSkia 其余部分对布尔标志则惯用#ifdef SK_MACRO风格。两种风格的区别在于:#if风格允许把开关值集中定义在一处并可取非,而#ifdef风格下宏“存在与否”本身就是开关。
大括号与流程控制
大括号位置
开括号不换行;else/else if与前后大括号同行,除非有预处理条件编译介入;if、else、while、for、do后必须使用大括号,即使只有一行:
if (...) { oneOrManyLines; } if (...) { oneOrManyLines; } else if (...) { oneOrManyLines; } else { oneOrManyLines; } for (...) { oneOrManyLines; } void function(...) { oneOrManyLines; } // 预处理条件编译打断 else 时的写法 if (!error) { proceed_as_usual(); } #if HANDLE_ERROR else { freak_out(); } #endif这与 .clang-format 中BreakBeforeBraces: Custom且所有BraceWrapping.*: false的设置完全一致(Allman/K&R 之外的定制风格:类、函数、控制语句之后均不换行开括号)。
控制关键字留白
流程控制关键字与左括号之间、括号与大括号之间都要留空格:
while (...) { } do { } while (...); switch (...) { ... }对应配置为SpaceBeforeParens: ControlStatements。
switch/case:缩进、fallthrough 与 case 内块
case与default相对switch缩进一级:
switch (color) { case kBlue: ... break; case kGreen: ... break; ... default: ... break; }跨 case 的隐式贯穿必须用[[fallthrough]]标注;但连续多个空 case 标签共享同一实现时不需要标注:
switch (recipe) { ... case kSmallCheesePizza_Recipe: case kLargeCheesePizza_Recipe: ingredients |= kCheese_Ingredient | kDough_Ingredient | kSauce_Ingredient; break; case kCheeseOmelette_Recipe: ingredients |= kCheese_Ingredient; [[fallthrough]] case kPlainOmelette_Recipe: ingredients |= (kEgg_Ingredient | kMilk_Ingredient); break; ... }这一约定在核心代码中真实存在,例如 src/core/SkBlurEngine.cpp:
case 2: [[fallthrough]]; case 3: [[fallthrough]];以及 src/core/SkBitmapProcState_matrixProcs.cpp 中的多 case 连续贯穿写法。
当某个 case 需要声明局部变量时,用花括号开块、} break;收尾:
switch (filter) { ... case kGaussian_Filter: { Bitmap srcCopy = src->makeCopy(); ... } break; ... };case缩进由 .clang-format 的IndentCaseLabels: true强制(注意:Google 基础风格默认不缩进 case,这里做了覆盖)。
类声明规范
可见性排序与成员分组
除非有前向声明需要,类声明中的可见性区应按public、protected、private顺序排列,每个可见性标签前留一个空行;同一可见性区内,数据字段与方法不要交叉混排,建议把所有数据字段集中放在区段末尾:
class SkFoo { public: ... protected: ... private: void barHelper(...); ... SkBar fBar; ... };对应配置AccessModifierOffset: -4:public:等标签向左缩进 4 列(顶格)。
override 与父类方法限定
被派生类重写的虚函数应使用override关键字并省略virtual:
void myVirtual() override { }当调用父类版本的同名方法需要明确体现时,用Parent::method();使用了作用域限定符时,不再需要this->:
class GrDillPickle : public GrPickle { ... bool onTasty() const override { return GrPickle::onTasty() && fFreshDill; } ... private: bool fFreshDill; };构造函数初始化列表格式
初始化器若能在同一行放下,就放在构造函数同行;否则每个初始化器独占一行、缩进对齐,逗号放在下一行行首:
GrDillPickle::GrDillPickle() : GrPickle(), fSize(kDefaultPickleSize) {} GrDillPickle::GrDillPickle(float size, float crunchiness, const PickleOptions* options) : GrPickle(options) , fSize(size) , fCrunchiness(crunchiness) {}这是 .clang-format 中三行配置组合出的效果:ConstructorInitializerAllOnOneLineOrOnePerLine: true(要么全在一行,要么一行一个)、ConstructorInitializerIndentWidth: 8(初始化器缩进 8 列)、BreakConstructorInitializersBeforeComma: true(逗号前置)。
explicit 单参构造函数
接受单一实参的构造函数几乎总是应声明为explicit,例外仅限极少数“自动兼容”类:
class Foo { explicit Foo(int x); // Good. Foo(float y); // Spooky implicit conversion from float to Foo. No no no! ... };this-> 前缀
在方法内部调用本对象的方法时,应显式加this->:
this->method();核心模式:公开非虚入口 + 私有 onMethodName 虚函数
Skia 中虚方法的一个标志性模式是:提供一个公开的非虚(或 final)入口方法,与之配对的是一个私有虚方法onMethodName。其设计目的是保证基类逻辑(前后置约束)一定被执行,由基类掌控虚方法的使用方式,而不是依赖每个子类自觉调用Parent::onMethodName():
class SkSandwich { public: void assemble() { // All sandwiches must have bread on the top and bottom. this->addIngredient(kBread_Ingredient); this->onAssemble(); this->addIngredient(kBread_Ingredient); } bool cook() { return this->onCook(); } private: // All sandwiches must implement onAssemble. virtual void onAssemble() = 0; // Sandwiches can remain uncooked by default. virtual bool onCook() { return true; } }; class SkGrilledCheese : public SkSandwich { private: void onAssemble() override { this->addIngredient(kCheese_Ingredient); } bool onCook() override { return this->toastOnGriddle(); } }; class SkPeanutButterAndJelly : public SkSandwich { private: void onAssemble() override { this->addIngredient(kPeanutButter_Ingredient); this->addIngredient(kGrapeJelly_Ingredient); } };从源码结构看,这种“入口 + on 前缀钩子”的组合在 Skia 各类可扩展组件(绘制、后端接口)中是普遍的组织方式,读者在src/gpu下检索virtual.*on[A-Z]即可看到大量实例。
整数类型与函数参数
整数类型
Skia 对整数类型的取舍遵循 Google C++ Style Guide 中 “Integer Types” 一节(旧代码正在逐步改造对齐)。要点:
- 默认使用
int;只有当确需保证位宽时,才使用stdint.h中的定宽类型(int32_t等); - 对“计数不为负”等语义,用断言(
SK_ASSERT等)而不是unsigned类型来表达; - 位域一律用
uint32_t,除非出于打包或性能原因必须更短。
参数传递惯例
- 必须存在的常量对象参数:const 引用传递;
- 可选的常量对象参数:const 指针传递;
- 会被修改的对象参数:非 const 指针传递;
- 非常引用传参极少使用。
// src and paint are optional void SkCanvas::drawBitmapRect(const SkBitmap& bitmap, const SkIRect* src, const SkRect& dst, const SkPaint* paint = nullptr); // metrics is mutable (it is changed by the method) SkScalar SkPaint::getFontMetrics(FontMetric* metrics, SkScalar scale) const;可选参数用“指针 + 默认 nullptr”表达,可变参数用裸指针表达——这一惯例与 Google C++ 风格“optional 参数优先用指针”的建议一致。
长参数列表的换行
参数一行放不下时允许两种排版方式:
方式一——溢出参数与首参对齐:
void drawBitmapRect(const SkBitmap& bitmap, const SkRect& dst, const SkPaint* paint = nullptr) { this->drawBitmapRectToRect(bitmap, nullptr, dst, paint, kNone_DrawBitmapRectFlag); }方式二——全部参数换行、缩进 8 个空格:
void drawBitmapRect( const SkBitmap& bitmap, const SkRect& dst, const SkPaint* paint = nullptr) { this->drawBitmapRectToRect( bitmap, nullptr, dst, paint, kNone_DrawBitmapRectFlag); }8 空格缩进对应配置ContinuationIndentWidth: 8与AllowAllParametersOfDeclarationOnNextLine: true;两种排版都合规,体现了规范“100 列以内、自行判断”的弹性。
用 clang-format 自动落实风格
上述大部分排版规则已由仓库根目录的 .clang-format 机器化。该文件同时包含 Cpp 与 ObjC 两个配置段(文件头注释特别提醒:修改 Cpp 段时必须同步修改 ObjC 段)。关键配置与文档规则的映射如下:
| 文档规则 | .clang-format 配置 |
|---|---|
| 行宽 100 列 | ColumnLimit: 100 |
| 4 空格缩进、禁用 Tab | IndentWidth: 4、TabWidth: 4、UseTab: Never |
开括号不换行、else同行 | BreakBeforeBraces: Custom+ 全部BraceWrapping.*: false |
| case 相对 switch 缩进 | IndentCaseLabels: true |
| 初始化列表“一行一个且逗号前置、缩进 8 列” | ConstructorInitializerAllOnOneLineOrOnePerLine: true、ConstructorInitializerIndentWidth: 8、BreakConstructorInitializersBeforeComma: true |
| 控制关键字后留空格 | SpaceBeforeParens: ControlStatements |
| include 字母序 | SortIncludes: true+IncludeCategories |
| 短函数/if/循环可单行 | AllowShortFunctionsOnASingleLine: All、AllowShortIfStatementsOnASingleLine: true、AllowShortLoopsOnASingleLine: true |
指针左对齐(SkBitmap *b风格) | PointerAlignment: Left |
配置文件头部注释给出了标准工作流:
# Typical usage is to apply this to the lines you've modified in a local # change. Make sure to install git-clang-format [1] by adding it to your # path and make it executable. # # Stage your changes with "git add" and then run: # $ git clang-format # You can optionally use the "--" file filter to restrict formatting to certain # files or directories. The tool will display the list of files that were # modified. These have been modified without being staged. You can review the # modifications using "git diff".即:git add暂存改动后运行git clang-format,工具只格式化你改动过的行(未暂存回文件),再用git diff复核。注意适用前提:由于部分客户端仍在用较老版本的 clang-format,配置中刻意只保留了 clang-format 10 及更早版本支持的选项(注释中记录了 Xcode 自带 clang-format 10、bin/clang-format为 11、Homebrew 安装为 14 的对应关系),本地格式化工具版本过高反而可能产生与 CI 不一致的噪音。
Python 脚本
Skia 构建与工具链中包含大量 Python 脚本(如 gn 工具目录 下的构建辅助脚本)。规范对它们的约定简洁明确:Python 代码遵循 Google Python Style Guide。
小结
Skia 的编码风格是“人读文档 + 机读配置”双层保障:Coding Style Guidelines 给出语义层面的判断标准(f/k/g前缀体系、onMethodName模式、参数传递策略),.clang-format 则把缩进、括号、换行、include 排序等确定性规则交给git clang-format强制执行。对贡献者而言,掌握前者的判断规则、依赖后者处理机械排版,是提交补丁前对齐项目风格的最短路径。
- 图形学
- 图像处理
【免费下载链接】skia
Skia is a complete 2D graphic library for drawing Text, Geometries, and Images.
相关推荐
CANN pyasc 项目编码规范全解:从 C++/MLIR 风格约束到 clang-format 与 clang-tidy 落地实践
CANN pyasc 项目编码规范全解:从 C++/MLIR 风格约束到 clang format 与 clang tidy 落地实践 本文档系统梳理 CANN
编程语言编译器人工智能CANNAscendF3D 编码规范:多组件 C++/Python/Markdown 代码风格约定与 clang-format、Black、Prettier 自动化格式化实践
F3D 编码规范:多组件 C++/Python/Markdown 代码风格约定与 clang format、Black、Prettier 自动化格式化实践 本文
3D渲染图形学桌面应用GraphQLBundle类型系统完全指南:掌握Object、Input、Enum等核心类型定义
GraphQLBundle类型系统完全指南:掌握Object、Input、Enum等核心类型定义 GraphQLBundle为Symfony应用提供了完整的Gr
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考