分类 软件开发 下的文章

HTML5 是第五个且是当前的 HTML 版本,它是用于在万维网上构建和呈现内容的标记语言。本文将帮助读者了解它。

HTML5 通过 W3C 和 Web 超文本应用技术工作组 Web Hypertext Application Technology Working Group 之间的合作发展起来。它是一个更高版本的 HTML,它的许多新元素可以使你的页面更加语义化和动态。它是为所有人提供更好的 Web 体验而开发的。HTML5 提供了很多的功能,使 Web 更加动态和交互。

HTML5 的新功能是:

  • 新标签,如 <header><section>
  • 用于 2D 绘图的 <canvas> 元素
  • 本地存储
  • 新的表单控件,如日历、日期和时间
  • 新媒体功能
  • 地理位置

HTML5 还不是正式标准(LCTT 译注:HTML5 已于 2014 年成为“推荐标准”),因此,并不是所有的浏览器都支持它或其中一些功能。开发 HTML5 背后最重要的原因之一是防止用户下载并安装像 Silverlight 和 Flash 这样的多个插件。

新标签和元素

  • 语义化元素: 图 1 展示了一些有用的语义化元素。
  • 表单元素: HTML5 中的表单元素如图 2 所示。
  • 图形元素: HTML5 中的图形元素如图 3 所示。
  • 媒体元素: HTML5 中的新媒体元素如图 4 所示。

图 1:语义化元素

图 2:表单元素

图 3:图形元素

图 4:媒体元素

HTML5 的高级功能

地理位置

这是一个 HTML5 API,用于获取网站用户的地理位置,用户必须首先允许网站获取他或她的位置。这通常通过按钮和/或浏览器弹出窗口来实现。所有最新版本的 Chrome、Firefox、IE、Safari 和 Opera 都可以使用 HTML5 的地理位置功能。

地理位置的一些用途是:

  • 公共交通网站
  • 出租车及其他运输网站
  • 电子商务网站计算运费
  • 旅行社网站
  • 房地产网站
  • 在附近播放的电影的电影院网站
  • 在线游戏
  • 网站首页提供本地标题和天气
  • 工作职位可以自动计算通勤时间

工作原理: 地理位置通过扫描位置信息的常见源进行工作,其中包括以下:

  • 全球定位系统(GPS)是最准确的
  • 网络信号 - IP地址、RFID、Wi-Fi 和蓝牙 MAC地址
  • GSM/CDMA 蜂窝 ID
  • 用户输入

该 API 提供了非常方便的函数来检测浏览器中的地理位置支持:

if (navigator.geolocation) {
// do stuff
}

getCurrentPosition API 是使用地理位置的主要方法。它检索用户设备的当前地理位置。该位置被描述为一组地理坐标以及航向和速度。位置信息作为位置对象返回。

语法是:

getCurrentPosition(showLocation, ErrorHandler, options);
  • showLocation:定义了检索位置信息的回调方法。
  • ErrorHandler(可选):定义了在处理异步调用时发生错误时调用的回调方法。
  • options (可选): 定义了一组用于检索位置信息的选项。

我们可以通过两种方式向用户提供位置信息:测地和民用。

  1. 描述位置的测地方式直接指向纬度和经度。
  2. 位置信息的民用表示法是人类可读的且容易理解。

如下表 1 所示,每个属性/参数都具有测地和民用表示。

图 5 包含了一个位置对象返回的属性集。

图5:位置对象属性

网络存储

在 HTML 中,为了在本机存储用户数据,我们需要使用 JavaScript cookie。为了避免这种情况,HTML5 已经引入了 Web 存储,网站利用它在本机上存储用户数据。

与 Cookie 相比,Web 存储的优点是:

  • 更安全
  • 更快
  • 存储更多的数据
  • 存储的数据不会随每个服务器请求一起发送。只有在被要求时才包括在内。这是 HTML5 Web 存储超过 Cookie 的一大优势。

有两种类型的 Web 存储对象:

  1. 本地 - 存储没有到期日期的数据。
  2. 会话 - 仅存储一个会话的数据。

如何工作: localStoragesessionStorage 对象创建一个 key=value 对。比如: key="Name"value="Palak"

这些存储为字符串,但如果需要,可以使用 JavaScript 函数(如 parseInt()parseFloat())进行转换。

下面给出了使用 Web 存储对象的语法:

  • 存储一个值:

    • localStorage.setItem("key1", "value1");
    • localStorage["key1"] = "value1";
  • 得到一个值:

    • alert(localStorage.getItem("key1"));
    • alert(localStorage["key1"]);
  • 删除一个值: -removeItem("key1");
  • 删除所有值:

    • localStorage.clear();

应用缓存(AppCache)

使用 HTML5 AppCache,我们可以使 Web 应用程序在没有 Internet 连接的情况下脱机工作。除 IE 之外,所有浏览器都可以使用 AppCache(截止至此时)。

应用缓存的优点是:

  • 网页浏览可以脱机
  • 页面加载速度更快
  • 服务器负载更小

cache manifest 是一个简单的文本文件,其中列出了浏览器应缓存的资源以进行脱机访问。 manifest 属性可以包含在文档的 HTML 标签中,如下所示:

<html manifest="test.appcache"> 
... 
</html>

它应该在你要缓存的所有页面上。

缓存的应用程序页面将一直保留,除非:

  1. 用户清除它们
  2. manifest 被修改
  3. 缓存更新

视频

在 HTML5 发布之前,没有统一的标准来显示网页上的视频。大多数视频都是通过 Flash 等不同的插件显示的。但 HTML5 规定了使用 video 元素在网页上显示视频的标准方式。

目前,video 元素支持三种视频格式,如表 2 所示。

下面的例子展示了 video 元素的使用:

<! DOCTYPE HTML>
<html>
<body>

<video src=" vdeo.ogg" width="320" height="240" controls="controls">

This browser does not support the video element.

</video>

</body>
</html>

例子使用了 Ogg 文件,并且可以在 Firefox、Opera 和 Chrome 中使用。要使视频在 Safari 和未来版本的 Chrome 中工作,我们必须添加一个 MPEG4 和 WebM 文件。

video 元素允许多个 source 元素。source 元素可以链接到不同的视频文件。浏览器将使用第一个识别的格式,如下所示:

<video width="320" height="240" controls="controls">
<source src="vdeo.ogg" type="video/ogg" />
<source src=" vdeo.mp4" type="video/mp4" />
<source src=" vdeo.webm" type="video/webm" />
This browser does not support the video element.
</video>

图6:Canvas 的输出

音频

对于音频,情况类似于视频。在 HTML5 发布之前,在网页上播放音频没有统一的标准。大多数音频也通过 Flash 等不同的插件播放。但 HTML5 规定了通过使用音频元素在网页上播放音频的标准方式。音频元素用于播放声音文件和音频流。

目前,HTML5 audio 元素支持三种音频格式,如表 3 所示。

audio 元素的使用如下所示:

<! DOCTYPE HTML>
<html>
<body>

<audio src=" song.ogg" controls="controls">

This browser does not support the audio element.

</video>

</body>
</html>

此例使用 Ogg 文件,并且可以在 Firefox、Opera 和 Chrome 中使用。要在 Safari 和 Chrome 的未来版本中使 audio 工作,我们必须添加一个 MP3 和 Wav 文件。

audio 元素允许多个 source 元素,它可以链接到不同的音频文件。浏览器将使用第一个识别的格式,如下所示:

<audio controls="controls">
<source src="song.ogg" type="audio/ogg" />
<source src="song.mp3" type="audio/mpeg" />

This browser does not support the audio element.

</audio>

画布(Canvas)

要在网页上创建图形,HTML5 使用 画布 API。我们可以用它绘制任何东西,并且它使用 JavaScript。它通过避免从网络下载图像而提高网站性能。使用画布,我们可以绘制形状和线条、弧线和文本、渐变和图案。此外,画布可以让我们操作图像中甚至视频中的像素。你可以将 canvas 元素添加到 HTML 页面,如下所示:

<canvas id="myCanvas" width="200" height="100"></canvas>

画布元素不具有绘制元素的功能。我们可以通过使用 JavaScript 来实现绘制。所有绘画应在 JavaScript 中。

<script type="text/javascript">
var c=document.getElementById("myCanvas");
var cxt=c.getContext("2d");
cxt.fillStyle="blue";
cxt.storkeStyle = "red";
cxt.fillRect(10,10,100,100);
cxt.storkeRect(10,10,100,100);
</script>

以上脚本的输出如图 6 所示。

你可以绘制许多对象,如弧、圆、线/垂直梯度等。

HTML5 工具

为了有效操作,所有熟练的或业余的 Web 开发人员/设计人员都应该使用 HTML5 工具,当需要设置工作流/网站或执行重复任务时,这些工具非常有帮助。它们提高了网页设计的可用性。

以下是一些帮助创建很棒的网站的必要工具。

  • HTML5 Maker: 用来在 HTML、JavaScript 和 CSS 的帮助下与网站内容交互。非常容易使用。它还允许我们开发幻灯片、滑块、HTML5 动画等。
  • Liveweave: 用来测试代码。它减少了保存代码并将其加载到屏幕上所花费的时间。在编辑器中粘贴代码即可得到结果。它非常易于使用,并为一些代码提供自动完成功能,这使得开发和测试更快更容易。
  • Font dragr: 在浏览器中预览定制的 Web 字体。它会直接载入该字体,以便你可以知道看起来是否正确。也提供了拖放界面,允许你拖动字形、Web 开放字体和矢量图形来马上测试。
  • HTML5 Please: 可以让我们找到与 HTML5 相关的任何内容。如果你想知道如何使用任何一个功能,你可以在 HTML Please 中搜索。它提供了支持的浏览器和设备的有用资源的列表,语法,以及如何使用元素的一般建议等。
  • Modernizr: 这是一个开源工具,用于给访问者浏览器提供最佳体验。使用此工具,你可以检测访问者的浏览器是否支持 HTML5 功能,并加载相应的脚本。
  • Adobe Edge Animate: 这是必须处理交互式 HTML 动画的 HTML5 开发人员的有用工具。它用于数字出版、网络和广告领域。此工具允许用户创建无瑕疵的动画,可以跨多个设备运行。
  • Video.js: 这是一款基于 JavaScript 的 HTML5 视频播放器。如果要将视频添加到你的网站,你应该使用此工具。它使视频看起来不错,并且是网站的一部分。
  • The W3 Validator: W3 验证工具测试 HTML、XHTML、SMIL、MathML 等中的网站标记的有效性。要测试任何网站的标记有效性,你必须选择文档类型为 HTML5 并输入你网页的 URL。这样做之后,你的代码将被检查,并将提供所有错误和警告。
  • HTML5 Reset: 此工具允许开发人员在 HTML5 中重写旧网站的代码。你可以使用这些工具为你网站的访问者提供一个良好的网络体验。

Palak Shah

作者是高级软件工程师。她喜欢探索新技术,学习创新概念。她也喜欢哲学。你可以通过 [email protected] 联系她。


via: http://opensourceforu.com/2017/06/introduction-to-html5/

作者:Palak Shah 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我不能为我的人生想出一个引子来,所以……

1 在 GitHub.com 上编辑代码

我想我要开始介绍的第一件事是多数人都已经知道的(尽管我一周之前还不知道)。

当你登录到 GitHub ,查看一个文件时(任何文本文件,任何版本库),右上方会有一只小铅笔。点击它,你就可以编辑文件了。 当你编辑完成后,GitHub 会给出文件变更的建议,然后为你 复刻 fork 该仓库并创建一个 拉取请求 pull request (PR)。

是不是很疯狂?它为你创建了一个复刻!

你不需要自己去复刻、拉取,然后本地修改,再推送,然后创建一个 PR。

不是一个真正的 PR

这对于修改错误拼写以及编辑代码时的一些糟糕的想法是很有用的。

2 粘贴图像

在评论和 工单 issue 的描述中并不仅限于使用文字。你知道你可以直接从剪切板粘贴图像吗? 在你粘贴的时候,你会看到图片被上传 (到云端,这毫无疑问),并转换成 markdown 显示的图片格式。

棒极了。

3 格式化代码

如果你想写一个代码块的话,你可以用三个反引号(`)作为开始 —— 就像你在浏览 精通 Markdown 时所学到的一样 —— 而且 GitHub 会尝试去推测你所写下的编程语言。

但如果你粘贴的像是 Vue、Typescript 或 JSX 这样的代码,你就需要明确指出才能获得高亮显示。

在首行注明 ``jsx`:

…这意味着代码段已经正确的呈现:

(顺便说一下,这些用法也可以用到 gist。 如果你给一个 gist 用上 .jsx 扩展名,你的 JSX 语法就会高亮显示。)

这里是所有被支持的语法的清单。

4 用 PR 中的魔法词来关闭工单

比方说你已经创建了一个用来修复 #234 工单的拉取请求。那么你就可以把 fixes #234 这段文字放在你的 PR 的描述中(或者是在 PR 的评论的任何位置)。

接下来,在合并 PR 时会自动关闭与之对应的工单。这是不是很酷?

这里是更详细的学习帮助

5 链接到评论

是否你曾经想要链接到一个特定的评论但却无从着手?这是因为你不知道如何去做到这些。不过那都过去了,我的朋友,我告诉你啊,点击紧挨着名字的日期或时间,这就是如何链接到一个评论的方法。

嘿,这里有 gaearon 的照片!

6 链接到代码

那么你想要链接到代码的特定行么。我了解了。

试试这个:在查看文件的时候,点击挨着代码的行号。

哇哦,你看到了么?URL 更新了,加上了行号!如果你按下 Shift 键并点击其他的行号,格里格里巴巴变!URL 再一次更新并且现在出现了行范围的高亮。

分享这个 URL 将会链接到这个文件的那些行。但等一下,链接所指向的是当前分支。如果文件发生变更了怎么办?也许一个文件当前状态的 永久链接 permalink 就是你以后需要的。

我比较懒,所以我已经在一张截图中做完了上面所有的步骤:

说起 URL…

7 像命令行一样使用 GitHub URL

使用 UI 来浏览 GitHub 有着很好的体验。但有些时候最快到达你想去的地方的方法就是在地址栏输入。举个例子,如果我想要跳转到一个我正在工作的分支,然后查看与 master 分支的差异,我就可以在我的仓库名称的后边输入 /compare/branch-name

这样就会访问到指定分支的 diff 页面。

然而这就是与 master 分支的 diff,如果我要与 develoment 分支比较,我可以输入 /compare/development...my-branch

对于你这种键盘快枪手来说,ctrl+Lcmd+L 将会向上跳转光标进入 URL 那里(至少在 Chrome 中是这样)。这(再加上你的浏览器会自动补全)能够成为一种在分支间跳转的便捷方式。

专家技巧:使用方向键在 Chrome 的自动完成建议中移动同时按 shift+delete 来删除历史条目(例如,一旦分支被合并后)。

(我真的好奇如果我把快捷键写成 shift + delete 这样的话,是不是读起来会更加容易。但严格来说 ‘+’ 并不是快捷键的一部分,所以我并不觉得这很舒服。这一点纠结让 整晚难以入睡,Rhonda。)

8 在工单中创建列表

你想要在你的 工单 issue 中看到一个复选框列表吗?

你想要在工单列表中显示为一个漂亮的 “2 of 5” 进度条吗?

很好!你可以使用这些的语法创建交互式的复选框:

 - [ ] Screen width (integer)
 - [x] Service worker support
 - [x] Fetch support
 - [ ] CSS flexbox support
 - [ ] Custom elements

它的表示方法是空格、破折号、再空格、左括号、填入空格(或者一个 x ),然后封闭括号,接着空格,最后是一些话。

然后你可以实际选中或取消选中这些框!出于一些原因这些对我来说看上去就像是技术魔法。你可以选中这些框! 同时底层的文本会进行更新。

他们接下来会想到什么魔法?

噢,如果你在一个 项目面板 project board 上有这些工单的话,它也会在这里显示进度:

如果在我提到“在一个项目面板上”时你不知道我在说些什么,那么你会在本页下面进一步了解。

比如,在本页面下 2 厘米的地方。

9 GitHub 上的项目面板

我常常在大项目中使用 Jira 。而对于个人项目我总是会使用 Trello 。我很喜欢它们两个。

当我学会 GitHub 的几周后,它也有了自己的项目产品,就在我的仓库上的 Project 标签,我想我会照搬一套我已经在 Trello 上进行的任务。

没有一个是有趣的任务

这里是在 GitHub 项目上相同的内容:

你的眼睛最终会适应这种没有对比的显示

出于速度的缘故,我把上面所有的都添加为 “ 备注 note ” —— 意思是它们不是真正的 GitHub 工单。

但在 GitHub 上,管理任务的能力被集成在版本库的其他地方 —— 所以你可能想要从仓库添加已有的工单到面板上。

你可以点击右上角的 添加卡片 Add Cards ,然后找你想要添加的东西。在这里,特殊的搜索语法就派上用场了,举个例子,输入 is:pr is:open 然后现在你可以拖动任何开启的 PR 到项目面板上,或者要是你想清理一些 bug 的话就输入 label:bug

亦或者你可以将现有的备注转换为工单。

再或者,从一个现有工单的屏幕上,把它添加到右边面板的项目上。

它们将会进入那个项目面板的分类列表,这样你就能决定放到哪一类。

在实现那些任务的同一个仓库下放置任务的内容有一个巨大(超大)的好处。这意味着今后的几年你能够在一行代码上做一个 git blame,可以让你找出最初在这个任务背后写下那些代码的根据,而不需要在 Jira、Trello 或其它地方寻找蛛丝马迹。

缺点

在过去的三周我已经对所有的任务使用 GitHub 取代 Jira 进行了测试(在有点看板风格的较小规模的项目上) ,到目前为止我都很喜欢。

但是我无法想象在 scrum(LCTT 译注:迭代式增量软件开发过程)项目上使用它,我想要在那里完成正确的工期估算、开发速度的测算以及所有的好东西怕是不行。

好消息是,GitHub 项目只有很少一些“功能”,并不会让你花很长时间去评估它是否值得让你去切换。因此要不要试试,你自己看着办。

无论如何,我听说过 ZenHub 并且在 10 分钟前第一次打开了它。它是对 GitHub 高效的延伸,可以让你估计你的工单并创建 epic 和 dependency。它也有 velocity 和 燃尽图 burndown chart 功能;这看起来可能是世界上最棒的东西了。

延伸阅读: GitHub help on Projects

10 GitHub 维基

对于一堆非结构化页面(就像维基百科一样), GitHub 维基 wiki 提供的(下文我会称之为 Gwiki)就很优秀。

结构化的页面集合并没那么多,比如说你的文档。这里没办法说“这个页面是那个页面的子页”,或者有像‘下一节’和‘上一节’这样的按钮。Hansel 和 Gretel 将会完蛋,因为这里没有面包屑导航(LCTT 译注:引自童话故事《糖果屋》)。

(边注,你有读过那个故事吗? 这是个残酷的故事。两个混蛋小子将饥肠辘辘的老巫婆烧死在她自己的火炉里。毫无疑问她是留下来收拾残局的。我想这就是为什么如今的年轻人是如此的敏感 —— 今天的睡前故事太不暴力了。)

继续 —— 把 Gwiki 拿出来接着讲,我输入一些 NodeJS 文档中的内容作为维基页面,然后创建一个侧边栏以模拟一些真实结构。这个侧边栏会一直存在,尽管它无法高亮显示你当前所在的页面。

其中的链接必须手动维护,但总的来说,我认为这已经很好了。如果你觉得有需要的话可以看一下

它将不会与像 GitBook(它使用了 Redux 文档)或定制的网站这样的东西相比较。但它八成够用了,而且它就在你的仓库里。

我是它的一个粉丝。

我的建议:如果你已经拥有不止一个 README.md 文件,并且想要一些不同的页面作为用户指南或是更详细的文档,那么下一步你就需要停止使用 Gwiki 了。

如果你开始觉得缺少的结构或导航非常有必要的话,去切换到其他的产品吧。

11 GitHub 页面(带有 Jekyll)

你可能已经知道了可以使用 GitHub 页面 Pages 来托管静态站点。如果你不知道的话现在就可以去试试。不过这一节确切的说是关于使用 Jekyll 来构建一个站点。

最简单的来说, GitHub 页面 + Jekyll 会将你的 README.md 呈现在一个漂亮的主题中。举个例子,看看我的 关于 github 中的 readme 页面:

点击 GitHub 上我的站点的 设置 settings 标签,开启 GitHub 页面功能,然后挑选一个 Jekyll 主题……

我就会得到一个 Jekyll 主题的页面

由此我可以构建一个主要基于易于编辑的 markdown 文件的静态站点,其本质上是把 GitHub 变成一个 CMS(LCTT 译注:内容管理系统)。

我还没有真正的使用过它,但这就是 React 和 Bootstrap 网站构建的过程,所以并不可怕。

注意,在本地运行它需要 Ruby (Windows 用户会彼此交换一下眼神,然后转头看向其它的方向。macOS 用户会发出这样这样的声音 “出什么问题了,你要去哪里?Ruby 可是一个通用平台!GEMS 万岁!”)。

(这里也有必要加上,“暴力或威胁的内容或活动” 在 GitHub 页面上是不允许的,因此你不能去部署你的 Hansel 和 Gretel 重启之旅了。)

我的意见

为了这篇文章,我对 GitHub 页面 + Jekyll 研究越多,就越觉得这件事情有点奇怪。

“拥有你自己的网站,让所有的复杂性远离”这样的想法是很棒的。但是你仍然需要在本地生成配置。而且可怕的是需要为这样“简单”的东西使用很多 CLI(LCTT 译注:命令行界面)命令。

我只是略读了入门部分的七页,给我的感觉像是我才是那个小白。此前我甚至从来没有学习过所谓简单的 “Front Matter” 的语法或者所谓简单的 “Liquid 模板引擎” 的来龙去脉。

我宁愿去手工编写一个网站。

老实说我有点惊讶 Facebook 使用它来写 React 文档,因为他们能够用 React 来构建他们的帮助文档,并且在一天之内预渲染到静态的 HTML 文件

他们所需要做的就是利用已有的 Markdown 文件,就像跟使用 CMS 一样。

我想是这样……

12 使用 GitHub 作为 CMS

比如说你有一个带有一些文本的网站,但是你并不想在 HTML 的标记中储存那些文本。

取而代之,你想要把这堆文本存放到某个地方,以便非开发者也可以很容易地编辑。也许要使用某种形式的版本控制。甚至还可能需要一个审查过程。

这里是我的建议:在你的版本库中使用 markdown 文件存储文本。然后在你的前端使用插件来获取这些文本块并在页面呈现。

我是 React 的支持者,因此这里有一个 <Markdown> 插件的示例,给出一些 markdown 的路径,它就会被获取、解析,并以 HTML 的形式呈现。

(我正在使用 marked npm 包来将 markdown 解析为 HTML。)

这里是我的示例仓库 /text-snippets,里边有一些 markdown 文件 。

(你也可以使用 GitHub API 来获取内容 —— 但我不确定你是否能搞定。)

你可以像这样使用插件:

如此,GitHub 就是你的 CMS 了,可以说,不管有多少文本块都可以放进去。

上边的示例只是在浏览器上安装好插件后获取 markdown 。如果你想要一个静态站点那么你需要服务器端渲染。

有个好消息!没有什么能阻止你从服务器中获取所有的 markdown 文件 (并配上各种为你服务的缓存策略)。如果你沿着这条路继续走下去的话,你可能会想要去试试使用 GitHub API 去获取目录中的所有 markdown 文件的列表。

奖励环节 —— GitHub 工具!

我曾经使用过一段时间的 Chrome 的扩展 Octotree,而且现在我推荐它。虽然不是吐血推荐,但不管怎样我还是推荐它。

它会在左侧提供一个带有树状视图的面板以显示当前你所查看的仓库。

这个视频中我了解到了 octobox ,到目前为止看起来还不错。它是一个 GitHub 工单的收件箱。这一句介绍就够了。

说到颜色,在上面所有的截图中我都使用了亮色主题,所以希望不要闪瞎你的双眼。不过说真的,我看到的其他东西都是黑色的主题,为什么我非要忍受 GitHub 这个苍白的主题呐?

这是由 Chrome 扩展 Stylish(它可以在任何网站使用主题)和 GitHub Dark 风格的一个组合。要完全黑化,那黑色主题的 Chrome 开发者工具(这是内建的,在设置中打开) 以及 Atom One Dark for Chrome 主题你肯定也需要。

Bitbucket

这些内容不适合放在这篇文章的任何地方,但是如果我不称赞 Bitbucket 的话,那就不对了。

两年前我开始了一个项目并花了大半天时间评估哪一个 git 托管服务更适合,最终 Bitbucket 赢得了相当不错的成绩。他们的代码审查流程遥遥领先(这甚至比 GitHub 拥有的指派审阅者的概念要早很长时间)。

GitHub 后来在这次审查竞赛中追了上来,干的不错。不幸的是在过去的一年里我没有机会再使用 Bitbucket —— 也许他们依然在某些方面领先。所以,我会力劝每一个选择 git 托管服务的人考虑一下 Bitbucket 。

结尾

就是这样!我希望这里至少有三件事是你此前并不知道的,祝好。

修订:在评论中有更多的技巧;请尽管留下你自己喜欢的技巧。真的,真心祝好。

(题图:orig08.deviantart.net)


via: https://hackernoon.com/12-cool-things-you-can-do-with-github-f3e0424cf2f0

作者:David Gilbertson 译者:softpaopao 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇教程的所有源代码都可以在 GitHub 上找到。

欢迎回到《OpenGL 与 Go 教程》。如果你还没有看过第一节,那就要回过头去看看那一节。

你现在应该能够创造一个漂亮的白色三角形,但我们不会把三角形当成我们游戏的基本单元,是时候把三角形变成正方形了,然后我们会做出一个完整的方格。

让我们现在开始做吧!

利用三角形绘制方形

在我们绘制方形之前,先把三角形变成直角三角形。打开 main.go 文件,把 triangle 的定义改成像这个样子:

triangle = []float32{
    -0.5, 0.5, 0,
    -0.5, -0.5, 0,
    0.5, -0.5, 0,
}

我们做的事情是,把最上面的顶点 X 坐标移动到左边(也就是变为 -0.5),这就变成了像这样的三角形:

Conway's Game of Life  - 右弦三角形

很简单,对吧?现在让我们用两个这样的三角形顶点做成正方形。把 triangle 重命名为 square,然后添加第二个倒置的三角形的顶点数据,把直角三角形变成这样的:

square = []float32{
    -0.5, 0.5, 0,
    -0.5, -0.5, 0,
    0.5, -0.5, 0,

    -0.5, 0.5, 0,
    0.5, 0.5, 0,
    0.5, -0.5, 0,
}

注意:你也要把在 maindraw 里面命名的 triangle 改为 square

我们通过添加三个顶点,把顶点数增加了一倍,这三个顶点就是右上角的三角形,用来拼成方形。运行它看看效果:

Conway's Game of Life - 两个三角形构成方形

很好,现在我们能够绘制正方形了!OpenGL 一点都不难,对吧?

在窗口中绘制方形格子

现在我们能画一个方形,怎么画 100 个吗?我们来创建一个 cell 结构体,用来表示格子的每一个单元,因此我们能够很灵活的选择绘制的数量:

type cell struct {
    drawable uint32

    x int
    y int
}

cell 结构体包含一个 drawable 属性,这是一个顶点数组对象,就像我们在之前创建的一样,这个结构体还包含 X 和 Y 坐标,用来表示这个格子的位置。

我们还需要两个常量,用来设定格子的大小和形状:

const (
    ...

    rows = 10
    columns = 10
)

现在我们添加一个创建格子的函数:

func makeCells() [][]*cell {
    cells := make([][]*cell, rows, rows)
    for x := 0; x < rows; x++ {
        for y := 0; y < columns; y++ {
            c := newCell(x, y)
            cells[x] = append(cells[x], c)
        }
    }

    return cells
}

这里我们创建多维的 切片 slice ,代表我们的游戏面板,用名为 newCell 的新函数创建的 cell 来填充矩阵的每个元素,我们待会就来实现 newCell 这个函数。

在接着往下阅读前,我们先花一点时间来看看 makeCells 函数做了些什么。我们创造了一个切片,这个切片的长度和格子的行数相等,每一个切片里面都有一个 细胞 cell 的切片,这些细胞的数量与列数相等。如果我们把 rowscolumns 都设定成 2,那么就会创建如下的矩阵:

[
    [cell, cell],
    [cell, cell]
]

还可以创建一个更大的矩阵,包含 10x10 个细胞:

[
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell],
    [cell, cell, cell, cell, cell, cell, cell, cell, cell, cell]
]

现在应该理解了我们创造的矩阵的形状和表示方法。让我们看看 newCell 函数到底是怎么填充矩阵的:

func newCell(x, y int) *cell {
    points := make([]float32, len(square), len(square))
    copy(points, square)

    for i := 0; i < len(points); i++ {
        var position float32
        var size float32
        switch i % 3 {
        case 0:
                size = 1.0 / float32(columns)
                position = float32(x) * size
        case 1:
                size = 1.0 / float32(rows)
                position = float32(y) * size
        default:
                continue
        }

        if points[i] < 0 {
                points[i] = (position * 2) - 1
        } else {
                points[i] = ((position + size) * 2) - 1
        }
    }

    return &cell{
        drawable: makeVao(points),

        x: x,
        y: y,
    }
}

这个函数里有很多内容,我们把它分成几个部分。我们做的第一件事是复制了 square 的定义。这让我们能够修改该定义,定制当前的细胞位置,而不会影响其它使用 square 切片定义的细胞。然后我们基于当前索引迭代 points 副本。我们用求余数的方法来判断我们是在操作 X 坐标(i % 3 == 0),还是在操作 Y 坐标(i % 3 == 1)(跳过 Z 坐标是因为我们仅在二维层面上进行操作),跟着确定细胞的大小(也就是占据整个游戏面板的比例),当然它的位置是基于细胞在 相对游戏面板的 X 和 Y 坐标。

接着,我们改变那些包含在 square 切片中定义的 0.50-0.5 这样的点。如果点小于 0,我们就把它设置成原来的 2 倍(因为 OpenGL 坐标的范围在 -11 之间,范围大小是 2),减 1 是为了归一化 OpenGL 坐标。如果点大于等于 0,我们的做法还是一样的,不过要加上我们计算出的尺寸。

这样做是为了设置每个细胞的大小,这样它就能只填充它在面板中的部分。因为我们有 10 行 10 列,每一个格子能分到游戏面板的 10% 宽度和高度。

最后,确定了所有点的位置和大小,我们用提供的 X 和 Y 坐标创建一个 cell,并设置 drawable 字段与我们刚刚操作 points 得到的顶点数组对象(vao)一致。

好了,现在我们在 main 函数里可以移去对 makeVao 的调用了,用 makeCells 代替。我们还修改了 draw,让它绘制一系列的细胞而不是一个 vao

func main() {
    ...

    // vao := makeVao(square)
    cells := makeCells()

    for !window.ShouldClose() {
        draw(cells, window, program)
    }
}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    // TODO

    glfw.PollEvents()
    window.SwapBuffers()
}

现在我们要让每个细胞知道怎么绘制出自己。在 cell 里面添加一个 draw 函数:

func (c *cell) draw() {
    gl.BindVertexArray(c.drawable)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square) / 3))
}

这看上去很熟悉,它很像我们之前在 vao 里写的 draw,唯一的区别是我们的 BindVertexArray 函数用的是 c.drawable,这是我们在 newCell 中创造的细胞的 vao

回到 main 中的 draw 函数上,我们可以循环每个细胞,让它们自己绘制自己:

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    for x := range cells {
        for _, c := range cells[x] {
            c.draw()
        }
    }

    glfw.PollEvents()
    window.SwapBuffers()
}

如你所见,我们循环每一个细胞,调用它的 draw 函数。如果运行这段代码,你能看到像下面这样的东西:

Conway's Game of Life - 全部格子

这是你想看到的吗?我们做的是在格子里为每一行每一列创建了一个方块,然后给它上色,这就填满了整个面板!

注释掉 for 循环,我们就可以看到一个明显独立的细胞,像这样:

// for x := range cells {
//     for _, c := range cells[x] {
//         c.draw()
//     }
// }

cells[2][3].draw()

Conway's Game of Life - 一个单独的细胞

这只绘制坐标在 (X=2, Y=3) 的格子。你可以看到,每一个独立的细胞占据着面板的一小块部分,并且负责绘制自己那部分空间。我们也能看到游戏面板有自己的原点,也就是坐标为 (X=0, Y=0) 的点,在窗口的左下方。这仅仅是我们的 newCell 函数计算位置的方式,也可以用右上角,右下角,左上角,中央,或者其它任何位置当作原点。

接着往下做,移除 cells[2][3].draw() 这一行,取消 for 循环的那部分注释,变成之前那样全部绘制的样子。

总结

好了,我们现在能用两个三角形画出一个正方形了,我们还有一个游戏的面板了!我们该为此自豪,目前为止我们已经接触到了很多零碎的内容,老实说,最难的部分还在前面等着我们!

在接下来的第三节,我们会实现游戏核心逻辑,看到很酷的东西!

回顾

这是这一部分教程中 main.go 文件的内容:

package main

import (
    "fmt"
    "log"
    "runtime"
    "strings"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

const (
    width  = 500
    height = 500

    vertexShaderSource = `
        #version 410
        in vec3 vp;
        void main() {
            gl_Position = vec4(vp, 1.0);
        }
    ` + "\x00"

    fragmentShaderSource = `
        #version 410
        out vec4 frag_colour;
        void main() {
            frag_colour = vec4(1, 1, 1, 1.0);
        }
    ` + "\x00"

    rows    = 10
    columns = 10
)

var (
    square = []float32{
        -0.5, 0.5, 0,
        -0.5, -0.5, 0,
        0.5, -0.5, 0,

        -0.5, 0.5, 0,
        0.5, 0.5, 0,
        0.5, -0.5, 0,
    }
)

type cell struct {
    drawable uint32

    x int
    y int
}

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()
    program := initOpenGL()

    cells := makeCells()
    for !window.ShouldClose() {
        draw(cells, window, program)
    }
}

func draw(cells [][]*cell, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    for x := range cells {
        for _, c := range cells[x] {
            c.draw()
        }
    }

    glfw.PollEvents()
    window.SwapBuffers()
}

func makeCells() [][]*cell {
    cells := make([][]*cell, rows, rows)
    for x := 0; x < rows; x++ {
        for y := 0; y < columns; y++ {
            c := newCell(x, y)
            cells[x] = append(cells[x], c)
        }
    }

    return cells
}

func newCell(x, y int) *cell {
    points := make([]float32, len(square), len(square))
    copy(points, square)

    for i := 0; i < len(points); i++ {
        var position float32
        var size float32
        switch i % 3 {
        case 0:
            size = 1.0 / float32(columns)
            position = float32(x) * size
        case 1:
            size = 1.0 / float32(rows)
            position = float32(y) * size
        default:
            continue
        }

        if points[i] < 0 {
            points[i] = (position * 2) - 1
        } else {
            points[i] = ((position + size) * 2) - 1
        }
    }

    return &cell{
        drawable: makeVao(points),

        x: x,
        y: y,
    }
}

func (c *cell) draw() {
    gl.BindVertexArray(c.drawable)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
}

// 初始化 glfw,返回一个可用的 Window
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4)
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
        panic(err)
    }
    window.MakeContextCurrent()

    return window
}

// 初始化 OpenGL 并返回一个可用的着色器程序
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }

    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

// 初始化并返回由 points 提供的顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

让我知道这篇文章对你有没有帮助,在 Twitter @kylewbanks 或者下方的连接,关注我以便获取最新的文章!


via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-2-drawing-the-game-board

作者:kylewbanks 译者:GitFtuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

变量是偷偷摸摸的。有时,它们会很高兴地呆在寄存器中,但是一转头就会跑到堆栈中。为了优化,编译器可能会完全将它们从窗口中抛出。无论变量在内存中的如何移动,我们都需要一些方法在调试器中跟踪和操作它们。这篇文章将会教你如何处理调试器中的变量,并使用 libelfin 演示一个简单的实现。

系列文章索引

  1. 准备环境
  2. 断点
  3. 寄存器和内存
  4. ELF 和 DWARF
  5. 源码和信号
  6. 源码级逐步执行
  7. 源码级断点
  8. 堆栈展开
  9. 处理变量
  10. 高级话题

在开始之前,请确保你使用的 libelfin 版本是我分支上的 fbreg。这包含了一些 hack 来支持获取当前堆栈帧的基址并评估位置列表,这些都不是由原生的 libelfin 提供的。你可能需要给 GCC 传递 -gdwarf-2 参数使其生成兼容的 DWARF 信息。但是在实现之前,我将详细说明 DWARF 5 最新规范中的位置编码方式。如果你想要了解更多信息,那么你可以从这里获取该标准。

DWARF 位置

某一给定时刻的内存中变量的位置使用 DW_AT_location 属性编码在 DWARF 信息中。位置描述可以是单个位置描述、复合位置描述或位置列表。

  • 简单位置描述:描述了对象的一个​​连续的部分(通常是所有部分)的位置。简单位置描述可以描述可寻址存储器或寄存器中的位置,或缺少位置(具有或不具有已知值)。比如,DW_OP_fbreg -32: 一个整个存储的变量 - 从堆栈帧基址开始的32个字节。
  • 复合位置描述:根据片段描述对象,每个对象可以包含在寄存器的一部分中或存储在与其他片段无关的存储器位置中。比如, DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2:前四个字节位于寄存器 3 中,后两个字节位于寄存器 10 中的一个变量。
  • 位置列表:描述了具有有限生存期或在生存期内更改位置的对象。比如:

    • <loclist with 3 entries follows>

      • [ 0]<lowpc=0x2e00><highpc=0x2e19>DW_OP_reg0
      • [ 1]<lowpc=0x2e19><highpc=0x2e3f>DW_OP_reg3
      • [ 2]<lowpc=0x2ec4><highpc=0x2ec7>DW_OP_reg2
    • 根据程序计数器的当前值,位置在寄存器之间移动的变量。

根据位置描述的种类,DW_AT_location 以三种不同的方式进行编码。exprloc 编码简单和复合的位置描述。它们由一个字节长度组成,后跟一个 DWARF 表达式或位置描述。loclistloclistptr 的编码位置列表,它们在 .debug_loclists 部分中提供索引或偏移量,该部分描述了实际的位置列表。

DWARF 表达式

使用 DWARF 表达式计算变量的实际位置。这包括操作堆栈值的一系列操作。有很多 DWARF 操作可用,所以我不会详细解释它们。相反,我会从每一个表达式中给出一些例子,给你一个可用的东西。另外,不要害怕这些;libelfin 将为我们处理所有这些复杂性。

  • 字面编码

    • DW_OP_lit0DW_OP_lit1……DW_OP_lit31

      • 将字面量压入堆栈
    • DW_OP_addr <addr>

      • 将地址操作数压入堆栈
    • DW_OP_constu <unsigned>

      • 将无符号值压入堆栈
  • 寄存器值

    • DW_OP_fbreg <offset>

      • 压入在堆栈帧基址找到的值,偏移给定值
    • DW_OP_breg0DW_OP_breg1…… DW_OP_breg31 <offset>

      • 将给定寄存器的内容加上给定的偏移量压入堆栈
  • 堆栈操作

    • DW_OP_dup

      • 复制堆栈顶部的值
    • DW_OP_deref

      • 将堆栈顶部视为内存地址,并将其替换为该地址的内容
  • 算术和逻辑运算

    • DW_OP_and

      • 弹出堆栈顶部的两个值,并压回它们的逻辑 AND
    • DW_OP_plus

      • DW_OP_and 相同,但是会添加值
  • 控制流操作

    • DW_OP_leDW_OP_eqDW_OP_gt

      • 弹出前两个值,比较它们,并且如果条件为真,则压入 1,否则为 0
    • DW_OP_bra <offset>

      • 条件分支:如果堆栈的顶部不是 0,则通过 offset 在表达式中向后或向后跳过
  • 输入转化

    • DW_OP_convert <DIE offset>

      • 将堆栈顶部的值转换为不同的类型,它由给定偏移量的 DWARF 信息条目描述
  • 特殊操作

    • DW_OP_nop

      • 什么都不做!

DWARF 类型

DWARF 类型的表示需要足够强大来为调试器用户提供有用的变量表示。用户经常希望能够在应用程序级别进行调试,而不是在机器级别进行调试,并且他们需要了解他们的变量正在做什么。

DWARF 类型与大多数其他调试信息一起编码在 DIE 中。它们可以具有指示其名称、编码、大小、字节等的属性。无数的类型标签可用于表示指针、数组、结构体、typedef 以及 C 或 C++ 程序中可以看到的任何其他内容。

以这个简单的结构体为例:

struct test{
    int i;
    float j;
    int k[42];
    test* next;
};

这个结构体的父 DIE 是这样的:

< 1><0x0000002a>    DW_TAG_structure_type
                      DW_AT_name                  "test"
                      DW_AT_byte_size             0x000000b8
                      DW_AT_decl_file             0x00000001 test.cpp
                      DW_AT_decl_line             0x00000001

上面说的是我们有一个叫做 test 的结构体,大小为 0xb8,在 test.cpp 的第 1 行声明。接下来有许多描述成员的子 DIE。

< 2><0x00000032>      DW_TAG_member
                        DW_AT_name                  "i"
                        DW_AT_type                  <0x00000063>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000002
                        DW_AT_data_member_location  0
< 2><0x0000003e>      DW_TAG_member
                        DW_AT_name                  "j"
                        DW_AT_type                  <0x0000006a>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000003
                        DW_AT_data_member_location  4
< 2><0x0000004a>      DW_TAG_member
                        DW_AT_name                  "k"
                        DW_AT_type                  <0x00000071>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000004
                        DW_AT_data_member_location  8
< 2><0x00000056>      DW_TAG_member
                        DW_AT_name                  "next"
                        DW_AT_type                  <0x00000084>
                        DW_AT_decl_file             0x00000001 test.cpp
                        DW_AT_decl_line             0x00000005
                        DW_AT_data_member_location  176(as signed = -80)

每个成员都有一个名称、一个类型(它是一个 DIE 偏移量)、一个声明文件和行,以及一个指向其成员所在的结构体的字节偏移。其类型指向如下。

< 1><0x00000063>    DW_TAG_base_type
                      DW_AT_name                  "int"
                      DW_AT_encoding              DW_ATE_signed
                      DW_AT_byte_size             0x00000004
< 1><0x0000006a>    DW_TAG_base_type
                      DW_AT_name                  "float"
                      DW_AT_encoding              DW_ATE_float
                      DW_AT_byte_size             0x00000004
< 1><0x00000071>    DW_TAG_array_type
                      DW_AT_type                  <0x00000063>
< 2><0x00000076>      DW_TAG_subrange_type
                        DW_AT_type                  <0x0000007d>
                        DW_AT_count                 0x0000002a
< 1><0x0000007d>    DW_TAG_base_type
                      DW_AT_name                  "sizetype"
                      DW_AT_byte_size             0x00000008
                      DW_AT_encoding              DW_ATE_unsigned
< 1><0x00000084>    DW_TAG_pointer_type
                      DW_AT_type                  <0x0000002a>

如你所见,我笔记本电脑上的 int 是一个 4 字节的有符号整数类型,float是一个 4 字节的浮点数。整数数组类型通过指向 int 类型作为其元素类型,sizetype(可以认为是 size_t)作为索引类型,它具有 2a 个元素。 test * 类型是 DW_TAG_pointer_type,它引用 test DIE。

实现简单的变量读取器

如上所述,libelfin 将为我们处理大部分复杂性。但是,它并没有实现用于表示可变位置的所有方法,并且在我们的代码中处理这些将变得非常复杂。因此,我现在选择只支持 exprloc。请根据需要添加对更多类型表达式的支持。如果你真的有勇气,请提交补丁到 libelfin 中来帮助完成必要的支持!

处理变量主要是将不同部分定位在存储器或寄存器中,读取或写入与之前一样。为了简单起见,我只会告诉你如何实现读取。

首先我们需要告诉 libelfin 如何从我们的进程中读取寄存器。我们创建一个继承自 expr_context 的类并使用 ptrace 来处理所有内容:

class ptrace_expr_context : public dwarf::expr_context {
public:
    ptrace_expr_context (pid_t pid) : m_pid{pid} {}

    dwarf::taddr reg (unsigned regnum) override {
        return get_register_value_from_dwarf_register(m_pid, regnum);
    }

    dwarf::taddr pc() override {
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, m_pid, nullptr, &regs);
        return regs.rip;
    }

    dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override {
        //TODO take into account size
        return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr);
    }

private:
    pid_t m_pid;
};

读取将由我们 debugger 类中的 read_variables 函数处理:

void debugger::read_variables() {
    using namespace dwarf;

    auto func = get_function_from_pc(get_pc());

    //...
}

我们上面做的第一件事是找到我们目前进入的函数,然后我们需要循环访问该函数中的条目来寻找变量:

    for (const auto& die : func) {
        if (die.tag == DW_TAG::variable) {
            //...
        }
    }

我们通过查找 DIE 中的 DW_AT_location 条目获取位置信息:

            auto loc_val = die[DW_AT::location];

接着我们确保它是一个 exprloc,并请求 libelfin 来评估我们的表达式:

            if (loc_val.get_type() == value::type::exprloc) {
                ptrace_expr_context context {m_pid};
                auto result = loc_val.as_exprloc().evaluate(&context);

现在我们已经评估了表达式,我们需要读取变量的内容。它可以在内存或寄存器中,因此我们将处理这两种情况:

                switch (result.location_type) {
                case expr_result::type::address:
                {
                    auto value = read_memory(result.value);
                    std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = "
                              << value << std::endl;
                    break;
                }

                case expr_result::type::reg:
                {
                    auto value = get_register_value_from_dwarf_register(m_pid, result.value);
                    std::cout << at_name(die) << " (reg " << result.value << ") = "
                              << value << std::endl;
                    break;
                }

                default:
                    throw std::runtime_error{"Unhandled variable location"};
                }

你可以看到,我根据变量的类型,打印输出了值而没有解释。希望通过这个代码,你可以看到如何支持编写变量,或者用给定的名字搜索变量。

最后我们可以将它添加到我们的命令解析器中:

    else if(is_prefix(command, "variables")) {
        read_variables();
    }

测试一下

编写一些具有一些变量的小功能,不用优化并带有调试信息编译它,然后查看是否可以读取变量的值。尝试写入存储变量的内存地址,并查看程序改变的行为。

已经有九篇文章了,还剩最后一篇!下一次我会讨论一些你可能会感兴趣的更高级的概念。现在你可以在这里找到这个帖子的代码。


via: https://blog.tartanllama.xyz/writing-a-linux-debugger-variables/

作者:Simon Brand 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

朋友们,你们好!

近来,我听到了大量的关于新出的 .NET Core 和其性能的讨论,尤其在 Web 服务方面的讨论更甚。

因为是新出的,我不想立马就比较两个不同的东西,所以我耐心等待,想等发布更稳定的版本后再进行。

本周一(8 月 14 日),微软发布 .NET Core 2.0 版本,因此,我准备开始。您们认为呢?

如前面所提的,我们会比较它们相同的东西,比如应用程序、预期响应及运行时的稳定性,所以我们不会把像对 JSON 或者 XML 的编码、解码这些烦多的事情加入比较游戏中来,仅仅只会使用简单的文本消息。为了公平起见,我们会分别使用 Go 和 .NET Core 的 MVC 架构模式

参赛选手

Go (或称 Golang): 是一种快速增长的开源编程语言,旨在构建出简单、快捷和稳定可靠的应用软件。

用于支持 Go 语言的 MVC web 框架并不多,还好我们找到了 Iris ,可胜任此工作。

Iris: 支持 Go 语言的快速、简单和高效的微型 Web 框架。它为您的下一代网站、API 或分布式应用程序奠定了精美的表现方式和易于使用的基础。

C#: 是一种通用的、面向对象的编程语言。其开发团队由 Anders Hejlsberg 领导。

.NET Core: 跨平台,可以在极少时间内开发出高性能的应用程序。

可从 https://golang.org/dl下载Go,从https://www.microsoft.com/net/core 下载 .NET Core。

在下载和安装好这些软件后,还需要为 Go 安装 Iris。安装很简单,仅仅只需要打开终端,然后执行如下语句:

go get -u github.com/kataras/iris

基准

硬件

  • 处理器: Intel(R) Core(TM) i7–4710HQ CPU @ 2.50GHz 2.50GHz
  • 内存: 8.00 GB

软件

两个应用程序都通过请求路径 “api/values/{id}” 返回文本“值”。

.NET Core MVC

Logo 由 Pablo Iglesias 设计。

可以使用 dotnet new webapi 命令创建项目,其 webapi 模板会为您生成代码,代码包含 GET 请求方法的 返回“值”

源代码:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace netcore_mvc
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace netcore_mvc
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMvc();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace netcore_mvc.Controllers
{
    // ValuesController is the equivalent
    // `ValuesController` of the Iris 8.3 mvc application.
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // Get handles "GET" requests to "api/values/{id}".
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // Put handles "PUT" requests to "api/values/{id}".
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // Delete handles "DELETE" requests to "api/values/{id}".
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

运行 .NET Core web 服务项目:

$ cd netcore-mvc
$ dotnet run -c Release
Hosting environment: Production
Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

运行和定位 HTTP 基准工具:

$ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5
Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections
 5000000 / 5000000 [=====================================================] 100.00% 2m3s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec     40226.03    8724.30     161919
  Latency        3.09ms     1.40ms   169.12ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:     8.91MB/s
Iris MVC

Logo 由 Santosh Anand 设计。

源代码:

package main

import (
    "github.com/kataras/iris"
    "github.com/kataras/iris/_benchmarks/iris-mvc/controllers"
)

func main() {
    app := iris.New()
    app.Controller("/api/values/{id}", new(controllers.ValuesController))
    app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker)
}
package controllers

import "github.com/kataras/iris/mvc"

// ValuesController is the equivalent
// `ValuesController` of the .net core 2.0 mvc application.
type ValuesController struct {
    mvc.Controller
}

// Get handles "GET" requests to "api/values/{id}".
func (vc *ValuesController) Get() {
    // id,_ := vc.Params.GetInt("id")
    vc.Ctx.WriteString("value")
}

// Put handles "PUT" requests to "api/values/{id}".
func (vc *ValuesController) Put() {}

// Delete handles "DELETE" requests to "api/values/{id}".
func (vc *ValuesController) Delete() {}

运行 Go web 服务项目:

$ cd iris-mvc
$ go run main.go
Now listening on: http://localhost:5000
Application started. Press CTRL+C to shut down.

运行和定位 HTTP 基准工具:

$ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5
Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections
 5000000 / 5000000 [======================================================] 100.00% 47s
Done!
Statistics        Avg      Stdev        Max
  Reqs/sec    105643.81    7687.79     122564
  Latency        1.18ms   366.55us    22.01ms
  HTTP codes:
    1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0
    others - 0
  Throughput:    19.65MB/s

想通过图片来理解的人,我也把我的屏幕截屏出来了!

请点击这儿可以看到这些屏幕快照。

总结

  • 完成 5000000 个请求的时间 - 越短越好。
  • 请求次数/每秒 - 越大越好。
  • 等待时间 — 越短越好。
  • 吞吐量 — 越大越好。
  • 内存使用 — 越小越好。
  • LOC (代码行数) — 越少越好。

.NET Core MVC 应用程序,使用 86 行代码,运行 2 分钟 8 秒,每秒接纳 39311.56 个请求,平均 3.19ms 等待,最大时到 229.73ms,内存使用大约为 126MB(不包括 dotnet 框架)。

Iris MVC 应用程序,使用 27 行代码,运行 47 秒,每秒接纳 105643.71 个请求,平均 1.18ms 等待,最大时到 22.01ms,内存使用大约为 12MB。

还有另外一个模板的基准,滚动到底部。

2017 年 8 月 20 号更新

Josh ClarkScott Hanselman在此 tweet 评论上指出,.NET Core Startup.cs 文件中 services.AddMvc(); 这行可以替换为 services.AddMvcCore();。我听从他们的意见,修改代码,重新运行基准,该文章的 .NET Core 应用程序的基准输出已经修改。

@topdawgevh @shanselman 他们也在使用 AddMvc() 而不是 AddMvcCore() ...,难道都不包含中间件?

 —  @clarkis117

@clarkis117 @topdawgevh Cool @MakisMaropoulos @benaadams @davidfowl 我们来看看。认真学习下怎么使用更简单的性能默认值。

 —  @shanselman

@shanselman @clarkis117 @topdawgevh @benaadams @davidfowl @shanselman @benaadams @davidfowl 谢谢您们的反馈意见。我已经修改,更新了结果,没什么不同。对其它的建议,我非常欢迎。

 —  @MakisMaropoulos

它有点稍微的不同但相差不大(从 8.61MB/s 到 8.91MB/s)

想要了解跟 services.AddMvc() 标准比较结果的,可以点击这儿

想再多了解点儿吗?

我们再制定一个基准,产生 1000000 次请求,这次会通过视图引擎由模板生成 HTML 页面。

.NET Core MVC 使用的模板

using System;

namespace netcore_mvc_templates.Models
{
    public class ErrorViewModel
    {
        public string Title { get; set; }
        public int Code { get; set; }
    }
}
 using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using netcore_mvc_templates.Models;

namespace netcore_mvc_templates.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View(new ErrorViewModel { Title = "Error", Code = 500});
        }
    }
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace netcore_mvc_templates
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace netcore_mvc_templates
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            /*  An unhandled exception was thrown by the application.
                System.InvalidOperationException: No service for type
                'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered.
                Solution: Use AddMvc() instead of AddMvcCore() in Startup.cs and it will work.
            */
            // services.AddMvcCore();
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}
/*
wwwroot/css
wwwroot/images
wwwroot/js
wwwroot/lib
wwwroot/favicon.ico


Views/Shared/_Layout.cshtml
Views/Shared/Error.cshtml

Views/Home/About.cshtml
Views/Home/Contact.cshtml
Views/Home/Index.cshtml

These files are quite long to be shown in this article but you can view them at: 
https://github.com/kataras/iris/tree/master/_benchmarks/netcore-mvc-templates

运行 .NET Core 服务项目:

$ cd netcore-mvc-templates
$ dotnet run -c Release
Hosting environment: Production
Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc-templates
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

运行 HTTP 基准工具:

Bombarding http://localhost:5000 with 1000000 requests using 125 connections
 1000000 / 1000000 [====================================================] 100.00% 1m20s
Done!
Statistics Avg Stdev Max
 Reqs/sec 11738.60 7741.36 125887
 Latency 10.10ms 22.10ms 1.97s
 HTTP codes:
 1xx — 0, 2xx — 1000000, 3xx — 0, 4xx — 0, 5xx — 0
 others — 0
 Throughput: 89.03MB/s

Iris MVC 使用的模板

package controllers

import "github.com/kataras/iris/mvc"

type AboutController struct{ mvc.Controller }

func (c *AboutController) Get() {
    c.Data["Title"] = "About"
    c.Data["Message"] = "Your application description page."
    c.Tmpl = "about.html"
}
package controllers

import "github.com/kataras/iris/mvc"

type ContactController struct{ mvc.Controller }

func (c *ContactController) Get() {
    c.Data["Title"] = "Contact"
    c.Data["Message"] = "Your contact page."
    c.Tmpl = "contact.html"
}
package models

// HTTPError a silly structure to keep our error page data.
type HTTPError struct {
    Title string
    Code  int
}
package controllers

import "github.com/kataras/iris/mvc"

type IndexController struct{ mvc.Controller }

func (c *IndexController) Get() {
    c.Data["Title"] = "Home Page"
    c.Tmpl = "index.html"
}
package main

import (
    "github.com/kataras/iris/_benchmarks/iris-mvc-templates/controllers"

    "github.com/kataras/iris"
    "github.com/kataras/iris/context"
)

const (
    // templatesDir is the exactly the same path that .NET Core is using for its templates,
    // in order to reduce the size in the repository.
    // Change the "C\\mygopath" to your own GOPATH.
    templatesDir = "C:\\mygopath\\src\\github.com\\kataras\\iris\\_benchmarks\\netcore-mvc-templates\\wwwroot"
)

func main() {
    app := iris.New()
    app.Configure(configure)

    app.Controller("/", new(controllers.IndexController))
    app.Controller("/about", new(controllers.AboutController))
    app.Controller("/contact", new(controllers.ContactController))

    app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker)
}

func configure(app *iris.Application) {
    app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html"))
    app.StaticWeb("/public", templatesDir)
    app.OnAnyErrorCode(onError)
}

type err struct {
    Title string
    Code  int
}

func onError(ctx context.Context) {
    ctx.ViewData("", err{"Error", ctx.GetStatusCode()})
    ctx.View("shared/error.html")
}
/*
../netcore-mvc-templates/wwwroot/css
../netcore-mvc-templates/wwwroot/images
../netcore-mvc-templates/wwwroot/js
../netcore-mvc-templates/wwwroot/lib
../netcore-mvc-templates/wwwroot/favicon.ico
views/shared/layout.html
views/shared/error.html
views/about.html
views/contact.html
views/index.html
These files are quite long to be shown in this article but you can view them at: 
https://github.com/kataras/iris/tree/master/_benchmarks/iris-mvc-templates
*/

运行 Go 服务项目:

$ cd iris-mvc-templates
$ go run main.go
Now listening on: http://localhost:5000
Application started. Press CTRL+C to shut down.

运行 HTTP 基准工具:

Bombarding http://localhost:5000 with 1000000 requests using 125 connections
 1000000 / 1000000 [======================================================] 100.00% 37s
Done!
Statistics Avg Stdev Max
 Reqs/sec 26656.76 1944.73 31188
 Latency 4.69ms 1.20ms 22.52ms
 HTTP codes:
 1xx — 0, 2xx — 1000000, 3xx — 0, 4xx — 0, 5xx — 0
 others — 0
 Throughput: 192.51MB/s

总结

  • 完成 1000000 个请求的时间 - 越短越好。
  • 请求次数/每秒 - 越大越好。
  • 等待时间 — 越短越好。
  • 内存使用 — 越小越好。
  • 吞吐量 — 越大越好。

.NET Core MVC 模板应用程序,运行 1 分钟 20 秒,每秒接纳 11738.60 个请求,同时每秒生成 89.03M 页面,平均 10.10ms 等待,最大时到 1.97s,内存使用大约为 193MB(不包括 dotnet 框架)。

Iris MVC 模板应用程序,运行 37 秒,每秒接纳 26656.76 个请求,同时每秒生成 192.51M 页面,平均 1.18ms 等待,最大时到 22.52ms,内存使用大约为 17MB。

接下来呢?

这里有上面所示的源代码,请下载下来,在您本地以同样的基准运行,然后把运行结果在这儿给大家分享。

想添加 Go 或 C# .net core WEB 服务框架到列表的朋友请向这个仓库_benchmarks 目录推送 PR。

我也需要亲自感谢下 dev.to 团队,感谢把我的这篇文章分享到他们的 Twitter 账户。

感谢大家真心反馈,玩得开心!

更新 : 2017 年 8 月 21 ,周一

很多人联系我,希望看到一个基于 .NET Core 的较低级别 Kestrel 的基准测试文章。

因此我完成了,请点击下面的链接来了解 Kestrel 和 Iris 之间的性能差异,它还包含一个会话存储管理基准!


via: https://hackernoon.com/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8

作者:Gerasimos Maropoulos 译者:runningwater 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇教程的所有源代码都可以在 GitHub 上找到。

介绍

OpenGL 是一门相当好的技术,适用于从桌面的 GUI 到游戏,到移动应用甚至 web 应用的多种类型的绘图工作。我敢保证,你今天看到的图形有些就是用 OpenGL 渲染的。可是,不管 OpenGL 多受欢迎、有多好用,与学习其它高级绘图库相比,学习 OpenGL 是要相当足够的决心的。

这个教程的目的是给你一个切入点,让你对 OpenGL 有个基本的了解,然后教你怎么用 Go 操作它。几乎每种编程语言都有绑定 OpenGL 的库,Go 也不例外,它有 go-gl 这个包。这是一个完整的套件,可以绑定 OpenGL ,适用于多种版本的 OpenGL。

这篇教程会按照下面列出的几个阶段进行介绍,我们最终的目标是用 OpenGL 在桌面窗口绘制游戏面板,进而实现康威生命游戏。完整的源代码可以在 GitHub github.com/KyleBanks/conways-gol 上获得,当你有疑惑的时候可以随时查看源代码,或者你要按照自己的方式学习也可以参考这个代码。

在我们开始之前,我们要先弄明白 康威生命游戏 Conway's Game of Life 到底是什么。这里是 Wikipedia 上面的总结:

《生命游戏》,也可以简称为 Life,是一个细胞自动变化的过程,由英国数学家 John Horton Conway 于 1970 年提出。

这个“游戏”没有玩家,也就是说它的发展依靠的是它的初始状态,不需要输入。用户通过创建初始配置文件、观察它如何演变,或者对于高级“玩家”可以创建特殊属性的模式,进而与《生命游戏》进行交互。

规则

《生命游戏》的世界是一个无穷多的二维正交的正方形细胞的格子世界,每一个格子都有两种可能的状态,“存活”或者“死亡”,也可以说是“填充态”或“未填充态”(区别可能很小,可以把它看作一个模拟人类/哺乳动物行为的早期模型,这要看一个人是如何看待方格里的空白)。每一个细胞与它周围的八个细胞相关联,这八个细胞分别是水平、垂直、斜对角相接的。在游戏中的每一步,下列事情中的一件将会发生:

  1. 当任何一个存活的细胞的附近少于 2 个存活的细胞时,该细胞将会消亡,就像人口过少所导致的结果一样
  2. 当任何一个存活的细胞的附近有 2 至 3 个存活的细胞时,该细胞在下一代中仍然存活。
  3. 当任何一个存活的细胞的附近多于 3 个存活的细胞时,该细胞将会消亡,就像人口过多所导致的结果一样
  4. 任何一个消亡的细胞附近刚好有 3 个存活的细胞,该细胞会变为存活的状态,就像重生一样。

不需要其他工具,这里有一个我们将会制作的演示程序:

Conway's Game of Life - 示例游戏

在我们的运行过程中,白色的细胞表示它是存活着的,黑色的细胞表示它已经死亡。

概述

本教程将会涉及到很多基础内容,从最基本的开始,但是你还是要对 Go 由一些最基本的了解 —— 至少你应该知道变量、切片、函数和结构体,并且装了一个 Go 的运行环境。我写这篇教程用的 Go 版本是 1.8,但它应该与之前的版本兼容。这里用 Go 语言实现没有什么特别新奇的东西,因此只要你有过类似的编程经历就行。

这里是我们在这个教程里将会讲到的东西:

最后的源代码可以在 GitHub 上获得,每一节的末尾有个回顾,包含该节相关的代码。如果有什么不清楚的地方或者是你感到疑惑的,看看每一节末尾的完整代码。

现在就开始吧!

安装 OpenGL 和 GLFW

我们介绍过 OpenGL,但是为了使用它,我们要有个窗口可以绘制东西。 GLFW 是一款用于 OpenGL 的跨平台 API,允许我们创建并使用窗口,而且它也是 go-gl 套件中提供的。

我们要做的第一件事就是确定 OpenGL 的版本。为了方便本教程,我们将会使用 OpenGL v4.1,但要是你的操作系统不支持最新的 OpenGL,你也可以用 v2.1。要安装 OpenGL,我们需要做这些事:

# 对于 OpenGL 4.1
$ go get github.com/go-gl/gl/v4.1-core/gl

# 或者 2.1
$ go get github.com/go-gl/gl/v2.1/gl

然后是安装 GLFW:

$ go get github.com/go-gl/glfw/v3.2/glfw

安装好这两个包之后,我们就可以开始了!先创建 main.go 文件,导入相应的包(我们待会儿会用到的其它东西)。

package main

import (
    "log"
    "runtime"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

接下来定义一个叫做 main 的函数,这是用来初始化 OpenGL 以及 GLFW,并显示窗口的:

const (
    width  = 500
    height = 500
)

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()

    for !window.ShouldClose() {
        // TODO
    }
}

// initGlfw 初始化 glfw 并且返回一个可用的窗口。
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
            panic(err)
    }

    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4) // OR 2
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
            panic(err)
    }
    window.MakeContextCurrent()

    return window
}

好了,让我们花一分钟来运行一下这个程序,看看会发生什么。首先定义了一些常量, widthheight —— 它们决定窗口的像素大小。

然后就是 main 函数。这里我们使用了 runtime 包的 LockOSThread(),这能确保我们总是在操作系统的同一个线程中运行代码,这对 GLFW 来说很重要,GLFW 需要在其被初始化之后的线程里被调用。讲完这个,接下来我们调用 initGlfw 来获得一个窗口的引用,并且推迟(defer)其终止。窗口的引用会被用在一个 for 循环中,只要窗口处于打开的状态,就执行某些事情。我们待会儿会讲要做的事情是什么。

initGlfw 是另一个函数,这里我们调用 glfw.Init() 来初始化 GLFW 包。然后我们定义了 GLFW 的一些全局属性,包括禁用调整窗口大小和改变 OpenGL 的属性。然后创建了 glfw.Window,这会在稍后的绘图中用到。我们仅仅告诉它我们想要的宽度和高度,以及标题,然后调用 window.MakeContextCurrent,将窗口绑定到当前的线程中。最后就是返回窗口的引用了。

如果你现在就构建、运行这个程序,你看不到任何东西。很合理,因为我们还没有用这个窗口做什么实质性的事。

定义一个新函数,初始化 OpenGL,就可以解决这个问题:

// initOpenGL 初始化 OpenGL 并且返回一个初始化了的程序。
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
            panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    prog := gl.CreateProgram()
    gl.LinkProgram(prog)
    return prog
}

initOpenGL 就像之前的 initGlfw 函数一样,初始化 OpenGL 库,创建一个 程序 program 。“程序”是一个包含了 着色器 shader 的引用,稍后会用 着色器 shader 绘图。待会儿会讲这一点,现在只用知道 OpenGL 已经初始化完成了,我们有一个程序的引用。我们还打印了 OpenGL 的版本,可以用于之后的调试。

回到 main 函数里,调用这个新函数:

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()

    program := initOpenGL()

    for !window.ShouldClose() {
        draw(window, program)
    }
}

你应该注意到了现在我们有 program 的引用,在我们的窗口循环中,调用新的 draw 函数。最终这个函数会绘制出所有细胞,让游戏状态变得可视化,但是现在它做的仅仅是清除窗口,所以我们只能看到一个全黑的屏幕:

func draw(window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(prog)

    glfw.PollEvents()
    window.SwapBuffers()
}

我们首先做的是调用 gl.clear 函数来清除上一帧在窗口中绘制的东西,给我们一个干净的面板。然后我们告诉 OpenGL 去使用我们的程序引用,这个引用还没有做什么事。最终我们告诉 GLFW 用 PollEvents 去检查是否有鼠标或者键盘事件(这一节里还不会对这些事件进行处理),告诉窗口去交换缓冲区 SwapBuffers交换缓冲区 很重要,因为 GLFW(像其他图形库一样)使用双缓冲,也就是说你绘制的所有东西实际上是绘制到一个不可见的画布上,当你准备好进行展示的时候就把绘制的这些东西放到可见的画布中 —— 这种情况下,就需要调用 SwapBuffers 函数。

好了,到这里我们已经讲了很多东西,花一点时间看看我们的实验成果。运行这个程序,你应该可以看到你所绘制的第一个东西:

Conway's Game of Life - 第一个窗口

完美!

在窗口里绘制三角形

我们已经完成了一些复杂的步骤,即使看起来不多,但我们仍然需要绘制一些东西。我们会以三角形绘制开始,可能这第一眼看上去要比我们最终要绘制的方形更难,但你会知道这样的想法是错的。你可能不知道的是三角形或许是绘制的图形中最简单的,实际上我们最终会用某种方式把三角形拼成方形。

好吧,那么我们想要绘制一个三角形,怎么做呢?我们通过定义图形的顶点来绘制图形,把它们交给 OpenGL 来进行绘制。先在 main.go 的顶部里定义我们的三角形:

var (
    triangle = []float32{
        0, 0.5, 0, // top
        -0.5, -0.5, 0, // left
        0.5, -0.5, 0, // right
    }
)

这看上去很奇怪,让我们分开来看。首先我们用了一个 float32 切片 slice ,这是一种我们总会在向 OpenGL 传递顶点时用到的数据类型。这个切片包含 9 个值,每三个值构成三角形的一个点。第一行, 0, 0.5, 0 表示的是 X、Y、Z 坐标,是最上方的顶点,第二行是左边的顶点,第三行是右边的顶点。每一组的三个点都表示相对于窗口中心点的 X、Y、Z 坐标,大小在 -11 之间。因此最上面的顶点 X 坐标是 0,因为它在 X 方向上位于窗口中央,Y 坐标是 0.5 意味着它会相对窗口中央上移 1/4 个单位(因为窗口的范围是 -11),Z 坐标是 0。因为我们只需要在二维空间中绘图,所以 Z 值永远是 0。现在看一看左右两边的顶点,看看你能不能理解为什么它们是这样定义的 —— 如果不能立刻就弄清楚也没关系,我们将会在屏幕上去观察它,因此我们需要一个完美的图形来进行观察。

好了,我们定义了一个三角形,但是现在我们得把它画出来。要画出这个三角形,我们需要一个叫做 顶点数组对象 Vertex Array Object 或者叫 vao 的东西,这是由一系列的点(也就是我们定义的三角形)创造的,这个东西可以提供给 OpenGL 来进行绘制。创建一个叫做 makeVao 的函数,然后我们可以提供一个点的切片,让它返回一个指向 OpenGL 顶点数组对象的指针:

// makeVao 执行初始化并从提供的点里面返回一个顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

首先我们创造了 顶点缓冲区对象 Vertex Buffer Object 或者说 vbo 绑定到我们的 vao 上,vbo 是通过所占空间(也就是 4 倍 len(points) 大小的空间)和一个指向顶点的指针(gl.Ptr(points))来创建的。你也许会好奇为什么它是 4 倍 —— 而不是 6 或者 3 或者 1078 呢?原因在于我们用的是 float32 切片,32 个位的浮点型变量是 4 个字节,因此我们说这个缓冲区以字节为单位的大小是点个数的 4 倍。

现在我们有缓冲区了,可以创建 vao 并用 gl.BindBuffer 把它绑定到缓冲区上,最后返回 vao。这个 vao 将会被用于绘制三角形!

回到 main 函数:

func main() {
    ...

    vao := makeVao(triangle)
    for !window.ShouldClose() {
        draw(vao, window, program)
    }
}

这里我们调用了 `makeVao` ,从我们之前定义的 `triangle` 顶点中获得 `vao` 引用,将它作为一个新的参数传递给 `draw` 函数:

func draw(vao uint32, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    gl.BindVertexArray(vao)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle) / 3))

    glfw.PollEvents()
    window.SwapBuffers()
}

然后我们把 OpenGL 绑定到 vao 上,这样当我们告诉 OpenGL 三角形切片的顶点数(除以 3,是因为每一个点有 X、Y、Z 坐标),让它去 DrawArrays ,它就知道要画多少个顶点了。

如果你这时候运行程序,你可能希望在窗口中央看到一个美丽的三角形,但是不幸的是你还看不到。还有一件事情没做,我们告诉 OpenGL 我们要画一个三角形,但是我们还要告诉它怎么画出来。

要让它画出来,我们需要叫做 片元着色器 fragment shader 顶点着色器 vertex shader 的东西,这些已经超出本教程的范围了(老实说,也超出了我对 OpenGL 的了解),但 Harold Serrano 在 Quora 上对对它们是什么给出了完美的介绍。我们只需要理解,对于这个应用来说,着色器是它内部的小程序(用 OpenGL Shader Language 或 GLSL 编写的),它操作顶点进行绘制,也可用于确定图形的颜色。

添加两个 import 和一个叫做 compileShader 的函数:

import (
    "strings"
    "fmt"
)

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

这个函数的目的是以字符串的形式接受着色器源代码和它的类型,然后返回一个指向这个编译好的着色器的指针。如果编译失败,我们就会获得出错的详细信息。

现在定义着色器,在 makeProgram 里编译。回到我们的 const 块中,我们在这里定义了 widthhegiht

vertexShaderSource = `
    #version 410
    in vec3 vp;
    void main() {
        gl_Position = vec4(vp, 1.0);
    }
` + "\x00"

fragmentShaderSource = `
    #version 410
    out vec4 frag_colour;
    void main() {
        frag_colour = vec4(1, 1, 1, 1);
    }
` + "\x00"

如你所见,这是两个包含了 GLSL 源代码字符串的着色器,一个是 顶点着色器 vertex shader ,另一个是 片元着色器 fragment shader 。唯一比较特殊的地方是它们都要在末尾加上一个空终止字符,\x00 —— OpenGL 需要它才能编译着色器。注意 fragmentShaderSource,这是我们用 RGBA 形式的值通过 vec4 来定义我们图形的颜色。你可以修改这里的值来改变这个三角形的颜色,现在的值是 RGBA(1, 1, 1, 1) 或者说是白色。

同样需要注意的是这两个程序都是运行在 #version 410 版本下,如果你用的是 OpenGL 2.1,那你也可以改成 #version 120。这里 120 不是打错的,如果你用的是 OpenGL 2.1,要用 120 而不是 210

接下来在 initOpenGL 中我们会编译着色器,把它们附加到我们的 program 中。

func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }
    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)    
    gl.LinkProgram(prog)
    return prog
}

这里我们用顶点着色器(vertexShader)调用了 compileShader 函数,指定它的类型是 gl.VERTEX_SHADER,对片元着色器(fragmentShader)做了同样的事情,但是指定的类型是 gl.FRAGMENT_SHADER。编译完成后,我们把它们附加到程序中,调用 gl.AttachShader,传递程序(prog)以及编译好的着色器作为参数。

现在我们终于可以看到我们漂亮的三角形了!运行程序,如果一切顺利的话你会看到这些:

Conway's Game of Life - Hello, Triangle!

总结

是不是很惊喜!这些代码画出了一个三角形,但我保证我们已经完成了大部分的 OpenGL 代码,在接下来的章节中我们还会用到这些代码。我十分推荐你花几分钟修改一下代码,看看你能不能移动三角形,改变三角形的大小和颜色。OpenGL 可以令人心生畏惧,有时想要理解发生了什么很困难,但是要记住,这不是魔法 - 它只不过看上去像魔法。

下一节里我们讲会用两个锐角三角形拼出一个方形 - 看看你能不能在进入下一节前试着修改这一节的代码。不能也没有关系,因为我们在 第二节 还会编写代码, 接着创建一个有许多方形的格子,我们把它当做游戏面板。

最后,在第三节 里我们会用格子来实现 Conway’s Game of Life

回顾

本教程 main.go 文件的内容如下:

package main

import (
    "fmt"
    "log"
    "runtime"
    "strings"

    "github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
    "github.com/go-gl/glfw/v3.2/glfw"
)

const (
    width  = 500
    height = 500

    vertexShaderSource = `
        #version 410
        in vec3 vp;
        void main() {
            gl_Position = vec4(vp, 1.0);
        }
    ` + "\x00"

    fragmentShaderSource = `
        #version 410
        out vec4 frag_colour;
        void main() {
            frag_colour = vec4(1, 1, 1, 1.0);
        }
    ` + "\x00"
)

var (
    triangle = []float32{
        0, 0.5, 0,
        -0.5, -0.5, 0,
        0.5, -0.5, 0,
    }
)

func main() {
    runtime.LockOSThread()

    window := initGlfw()
    defer glfw.Terminate()
    program := initOpenGL()

    vao := makeVao(triangle)
    for !window.ShouldClose() {
        draw(vao, window, program)
    }
}

func draw(vao uint32, window *glfw.Window, program uint32) {
    gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
    gl.UseProgram(program)

    gl.BindVertexArray(vao)
    gl.DrawArrays(gl.TRIANGLES, 0, int32(len(triangle)/3))

    glfw.PollEvents()
    window.SwapBuffers()
}

// initGlfw 初始化 glfw 并返回一个窗口供使用。
func initGlfw() *glfw.Window {
    if err := glfw.Init(); err != nil {
        panic(err)
    }
    glfw.WindowHint(glfw.Resizable, glfw.False)
    glfw.WindowHint(glfw.ContextVersionMajor, 4)
    glfw.WindowHint(glfw.ContextVersionMinor, 1)
    glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
    glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)

    window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
    if err != nil {
        panic(err)
    }
    window.MakeContextCurrent()

    return window
}

// initOpenGL 初始化 OpenGL 并返回一个已经编译好的着色器程序
func initOpenGL() uint32 {
    if err := gl.Init(); err != nil {
        panic(err)
    }
    version := gl.GoStr(gl.GetString(gl.VERSION))
    log.Println("OpenGL version", version)

    vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
    if err != nil {
        panic(err)
    }

    fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
    if err != nil {
        panic(err)
    }

    prog := gl.CreateProgram()
    gl.AttachShader(prog, vertexShader)
    gl.AttachShader(prog, fragmentShader)
    gl.LinkProgram(prog)
    return prog
}

// makeVao 执行初始化并从提供的点里面返回一个顶点数组
func makeVao(points []float32) uint32 {
    var vbo uint32
    gl.GenBuffers(1, &vbo)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)

    var vao uint32
    gl.GenVertexArrays(1, &vao)
    gl.BindVertexArray(vao)
    gl.EnableVertexAttribArray(0)
    gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
    gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

    return vao
}

func compileShader(source string, shaderType uint32) (uint32, error) {
    shader := gl.CreateShader(shaderType)

    csources, free := gl.Strs(source)
    gl.ShaderSource(shader, 1, csources, nil)
    free()
    gl.CompileShader(shader)

    var status int32
    gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
    if status == gl.FALSE {
        var logLength int32
        gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)

        log := strings.Repeat("\x00", int(logLength+1))
        gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))

        return 0, fmt.Errorf("failed to compile %v: %v", source, log)
    }

    return shader, nil
}

请在 Twitter @kylewbanks 上告诉我这篇文章对你是否有帮助,或者点击下方的关注,以便及时获取最新文章!


via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl

作者:kylewbanks 译者:GitFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出