WebGL Tutorial
and more

在NetBeans中一键编译

撰写时间:2024-11-12

修订时间:2024-11-13

使用Emscripten编译的问题

使用Emscripten的过程,实质上就是一个编译并运行C语言程序的过程,并且,还要加上与Web端相整合的步骤。因此,不像想像中那么轻松。

其过程主要涉及这几个环节:

  1. 在系统中设置PATH及相应变量名称
  2. IDE中编写并保存.c源代码
  3. 在命令行编译为相关文件
  4. 部署到服务器
  5. 打开浏览器并访问相关的网页
  6. 在学习、研究过程中需要分类保存不同的代码

有没有比较简便的操作,比如,修改好代码后,一键就可实现自动设置环境变量、保存、编译、部署、显示最终的HTML页面?

有。

首先,我们需要解决架设服务器的问题。我使用NetBeans,并已经安装好了PHP的环境。这样,在NetBeans中新建一个PHP项目,则就会自动在本地上部署并运行项目。因此,我将结合PHP项目的特性来讲解如何实现目标。

需要部署的路径结构

先看我们最终需要达成目标的文件路径结构。

  • webassembly
    • examples
      • playgrounds
        • deploy
          • wasm-playground.html
        • script
          • build.sh
        • src
          • hello.c
      • hello-world
      • [... any other demo folders]

我们相关的代码准备集中放在webassembly/examples下面。如上图所见,该路径下已有playgrounds, hello-world以及其他的项目文件夹,分别存放不同子项目的源代码。现在,我们准备对playgrounds子项目进行编译及部署。

该项目文件夹下有3个子文件夹。其中src用以存放C语言源代码,script用以存放帮我们自动编译的脚本,deploy存放最终在Web上呈现时所需要用到的各个文件。

根据上面各节的内容可知,Emscripten所自动生成的.html并不总是符合我们的要求,因此我们在了解其接口结构后可以根据需求定制所需版本。wasm-playground.html的内容很简单,其body的代码如下:

<body> <article> <h1>WASM Playground</h1> <p id="output"></p> </article> <script> var Module = { print: (() => { let output = document.getElementById('output'); return (...args) => { let text = args.join(' '); output.innerHTML += `<p>${text}</p>`; }; })(), printErr: function(text) { if (text.includes('application/wasm') || text.includes('falling back to ArrayBuffer instantiation')) { //console.info(text); return; } console.error(text); } }; </script> <script async src="hello.js"></script> </body>

只是简单地设置了printprintErr的两个回调函数的内容。

hello.c的内容:

#include <stdio.h> #include <emscripten.h> int main() { int num = 5; int total = EM_ASM_INT({ let result = $0 + 30; console.log(`Finished calculating`); return result; }, num); printf("%d\n", total + 30); return 0; }

因为我们已经有了自己的.html,因此我们只需编译生成.js.wasm2个文件并放置进deploy目录就行了。下面是build.sh脚本的主要代码:

... emcc src/hello.c -o deploy/hello.js

要编译成功,前提是我们需事先设置好了emcc的环境路径,并且当前工作路径在playgrounds路径下面。这些细节下面将谈到。

NetBeans的一键运行功能

NetBeans为不同类型的项目,均提供了一键运行的功能,特别方便,这里有初步的介绍。但对于PHP项目来讲,则需要一些额外的步骤。

我们先来实现最简单的一键运行效果。

  1. 先在PHP项目的一个对外界来讲是安全的路径下,生成一个emcc-dev.php文件。内容先为空。

    何为对外界安全的路径?一个Web项目总有一个根目录,这个根目录就是存放访问者访问您的网站时所看到的首页的目录。如本站的Apache服务器管理必读所述,诸如wwwpublic_html等命名的文件夹都是比较常见的Web根目录。重要的私密文件,不能放在这些目录下面。

    而如果不是Web根目录的目录,则是安全的,外界不可以访问,但PHP项目管理器可以进行安全的内部访问。

  2. NetBeans的工具栏上点击Set Project Configuration下拉箭头,

    在弹出的菜单中点击Customize...

  3. 弹出Project Properties...窗口:

    点击New ...按钮。

  4. 弹出Create New Configuration窗口:

    在文本框中填写emcc-dev,单击OK按钮。

  5. 返回至Project Properties窗口:

    Configuration一栏中已自动选择了刚刚生成的emcc-dev,下面是其各项设置。

    Run As:一栏中,选Script (run in command line)。其意思是,在命令行运行特定的PHP文件。PHP InterpreterNetBeans根据系统设置帮我们自动填写的,可不管它。Index File一栏,选择我们在第一步中所创建的emcc-dev.php文件。其他栏目留空。

    Arguments:一栏,是可以填写要传给emcc-dev.php文件的参数的。但因为我们希望实现按下同一按钮而运行不同子项目的效果,每个子项目若都在这里设置,反倒较累,后面有更好的解决方案,因此这里可以不填。

    单击OK按钮,关闭该窗口。设置完毕。

  6. NetBeans的工具栏上显示了刚创建的运行项:

    点击上面使用红框标出的运行按钮,NetBeans将自动打开一个Output窗口,并显示:

    "/Applications/MAMP/bin/php/php8.2.0/bin/php" "< YOUR_PHP_PROJECT_DIR >/others/etc/shell-scripts/emcc-dev.php" Done.

    发生了什么?PHP引擎运行了我们上面所创建的emcc-dev.php文件。当然,该文件内容目录为空,因此,什么都不会发生。

  7. 修改emcc-dev.php文件内容为: <?php system("Open -u http://localhost");

    PHP的代码通过调用system函数,执行了系统的Shell脚本命令,在浏览器中打开了连向http://localhost的网页。

上面这些步骤,我们实现了在NetBeans中只需一键即可在浏览器立即访问特定网页的功能。但这是最后的结果,在此之前,我们还需要实现两步:一是自动设置Emscripten的环境变量,二是调用emcc命令来编译。

设置Emscripten的环境变量

因为设置Emscripten的环境变量是通用的,因此,我们在上面emcc-dev.php文件所在目录创建一个名为export-emcc.sh的文件,内容如下:

export EMSDK="/Volumes/SarkuyaData/Programming/WebAssembly/Emscripten/emsdk" export EMSDK_NODE=$EMSDK/node/20.18.0_64bit/bin/node export EMSDK_PYTHON=$EMSDK/python/3.9.2_64bit/bin/python3 export PATH=$PATH:$EMSDK export PATH=$PATH:$EMSDK/upstream/emscripten

根据您系统的设置予以相应的修改。

这是在Unix, Linux系统下设置环境变量及路径的命令。有此设置,将让我们在不同的目录下方便地调用emcc进行编译。

修改emcc-dev.php的内容如下:

<?php system("source ./export-emcc.sh"); system("emcc -v"); system("Open -u http://localhost");

上面代码,我们的意图是,设置环境变量,调用emcc命令以查看其版本号,然后再在浏览器中访问网页。

一键运行。但结果是Output窗口出现了红色警示:

sh: emcc: command not found

意为,找不到emcc命令。这是因为环境变量的设置,只在PHPsystem函数所孵化的线程内有效。将上面内容修改为:

<?php $cmd_array = [ "source ./export-emcc.sh", "emcc -v", "Open -u http://localhost" ]; $cmds_bulk = ""; foreach($cmd_array as $cmd) { $cmds_bulk = $cmds_bulk . $cmd . "; "; } system($cmds_bulk);

将上面的3个命令,先放在一个数组中,然后再组合为一个字符串,最后让system函数整体执行。

一键运行,环境变量设置成功,打印了版本信息,浏览器正常访问网页。成功。但emcc的版本信息是红色的,这是其本性,没有错误,不用管它。

编译代码

终于来到最后一步了。现在,既然环境变量已设置好,我们希望调用上面所提到的build.sh脚本文件,让其帮助我们编译。再次修改emcc-dev.php内容如下:

$cmd_array = [ "source ./export-emcc.sh", "source /< YOUR_PATH_SETTINGS >/webassembly/examples/playgrounds/script/build.sh", "Open -u http://localhost" ];

为方便说明,这里使用了绝对路径的方式,将上面YOUR_PATH_SETTINGS改为您自己的路径。

一键运行。出现红色警示:

emcc: error: src/hello.c: No such file or directory

这回的红色真是错误信息了,意为,emcc找不到src/hello.c这个文件。

build.sh文件的最上面加入一行以打印当前工作路径:

pwd # added emcc src/hello.c -o deploy/hello.js

我们发现,当前工作路径为emcc-dev.php的路径,而不是build.sh所在的路径。因此,上面使用相对路径来查找hello.c,难怪找不到。

修改build.sh文件内容如下:

script_dir=`dirname $(readlink -f ${BASH_SOURCE[0]})` cd $script_dir cd .. emcc src/hello.c -o deploy/hello.js

其实系统自然是了解build.sh的绝对路径的,上面的第一行代码,我们向系统求助:我正在访问这个文件,请将这个文件的绝路径告诉我。得到路径后,使用cd命令,将当前工作路径转入其下面,再转到其父目录,这样,emcc就可将src/hello.c编译进deploy目录下面了。

一键运行,红色的错误警示信息消失。

真正就差最后一步了:浏览器中访问的网页还不是最终的网页。

上面,主要涉及两个文件的路径。一是脚本的路径,使用绝对路径的方式。另一种是网页的路径,使用URL的方式。在同一个PHP项目内,它们有其相应的关系。为使这个一键运行更有通用性,我们再次修改emcc-dev.php的内容如下:

<?php $doc_root = "< YOUR_DOC_ROOT >"; $web_root = "http://localhost"; $doc_base_url = "/docs/javascript/webassembly/examples"; $doc_branch = "/playgrounds"; $script_path = "/script"; $depoly_path = "/deploy"; $script_name = "build.sh"; $webpage_name = "wasm-playground.html"; $script_full = $doc_root . $doc_base_url . $doc_branch . $script_path . "/" . $script_name; $webpage_full = $web_root . $doc_base_url . $doc_branch . $depoly_path . "/" . $webpage_name; $cmd_array = array( "source ./export-emcc.sh", "source {$script_full}", "open -u {$webpage_full}" ); $cmds_bulk = ""; foreach($cmd_array as $cmd) { $cmds_bulk = $cmds_bulk . $cmd . "; "; } system($cmds_bulk);

PHP帮我们自动组装成相应的脚本路径及URL网址。将$doc_root改为您自己的路径。

上面各个变量,遵循我们自己所定下的默认约定,一般不予更改。例如,将各个子项目统一放在$doc_base_url下面;使用$script_path存放脚本文件,且脚本名称默认为build.sh;部署路径默认为deploy

而对于各个子项目,只需修改两个变量。$doc_branch为各个子项目名称,$webpage_name为打开浏览器时自动访问的网页名称。有了这样的约定,以后如果增加了子项目,仅修改这两个地方就可以了,还是很方便的。

效果检验

好了,一切准备妥当。看一下效果。将关注焦点放在hello.c上面。在NetBeans中修改其源代码。

#include <stdio.h> #include <emscripten.h> int main() { int num = 10; int total = EM_ASM_INT({ let result = $0 + 30; console.log(`Finished calculating`); return result; }, num); printf("%d\n", total + 30); return 0; }

上面代码的效果,是对变量num的值分别加上了230。将其值修改为20,然后,一键运行。不到1秒,网页就自动显示出来了:查看结果。神速!

改进:第2版

上面最大的问题在于,将doc_branch,也即子项目的名称,藏进了emcc-dev.php中,因此如果需要切换至不同的子项目,必须修改后者的内容。并且,与子项目相关的变量,也均写进了其中,模糊、扭曲了其作为公共调用接口的性质。

现在,我们制作第2版。在此版本中,我们通过界面来选择子项目,并且将子项目可能需要改变的地方集中放置于一个地方。

最终部署后的示意图如下:

  • webassembly
    • examples
      • using-template
        • deploy
          • demo.html
          • demo.js
          • demo.wasm
        • html-template
          • template.html
        • script
          • build.php
          • build.sh
        • src
          • hello.c
      • [... any other demo folders]

我们准备使用模板来生成上面的结构。script下面的build.phpNetBeans中得以一键运行的所需文件,build.sh是使用zsh来实际编译的脚本。

NetBeans中新建一个名为emcc-compileProject Configuation,如下图所示:

Index File一栏,我们通过界面上的Browse...按钮,很方便地选择了using-template子项目下面的build.php文件。以后要切换不同的子项目,只需在该栏重新选择相应子项目路径下的srcript/build.php即可。无需再改脚本内容。

Arguments一栏,我们以相对路径指定了def-vars.sh的文件,该文件将以参数的形式,由build.phpbuild.sh传递。这样,不同的子项目,就可以自动获取到其路径并予以调用。该设置对所有子项目通用,无需更改。

依上面配置完后,一键运行时,NetBeans将以命令行的方式来调用build.php,该文件内容为:

<?php $curr_php = $_SERVER['PHP_SELF']; $def_file = $argv[1]; $pattern = '/(.+)\/wwwroot.+/i'; $replacement = '$1'; $project_dir = preg_replace($pattern, $replacement, $curr_php); $def_full = $project_dir . "/" . $def_file; system(". build.sh $project_dir $def_full");

$curr_phpbuild.php的绝对路径,$def_file使用$argv[1]用于接收上面界面中的Arguments所指定的others/etc/shell-scripts/def-vars.sh字符串,这是def-vars.sh的相对路径,使用Perl流派的正则表达式将其转换为PHP项目路径及其def-vars.sh的绝对路径。

在此例子中,PHP项目的一级子目录结构如下:

  • SarkuyaCom
    • others
    • wwwroot

代码:

system(". build.sh $project_dir $def_full");

用于加载并运行build.sh脚本文件,并将project_dirdef_full作为参数传给该脚本。类似于:

system("source build.sh $project_dir $def_full");

Apple在其Including One Shell Script Inside Another (Sourcing)一文中讲到,使用.有较好的兼容性。

build.sh文件内容:

#!/bin/zsh config() { c_src_file=hello.c html_template=template.html output_name=demo } set_vars() { project_dir=$1 def_full=$2 . $def_full $project_dir } compile() { echo "3. Compiling..." emcc ../src/${c_src_file} -o ../deploy/${output_name}.html --shell-file ../html-template/${html_template} } open_url() { echo "4. Open browser to visit the url..." cd .. url=`pwd | sed "s#${doc_root}#${local_url_root}#"` url+="/deploy/${output_name}.html" Open -u ${url} } project_dir=$1 def_full=$2 set_vars $project_dir $def_full config compile open_url

为使用代码更清晰,这里使用了脚本函数。

上面代码在接收了project_dirdef_full参数后,在set_vars函数中,以变量$project_dir作为参数运行变量名为$def_full脚本文件,也即def-vars.sh文件。该文件内容如下:

#!/bin/zsh define_vars() { echo "1. Setting environment variables..." script_dir=`dirname $(readlink -f ${BASH_SOURCE[0]})` . $script_dir/export-emcc.sh echo "2. Setting variables for development..." # project_dir=`echo $script_dir | sed "s#${rev_dir}##"` project_dir=$1 doc_root="${project_dir}/wwwroot" local_url_root="http://localhost" } project_dir=$1 define_vars $project_dir

上面代码为后面的脚本设置了emcc的环境变量及代码变量doc_rootlocal_url_root。在从def-vars.sh返回后,build.sh则可方便地使用这些变量。

回到build.sh脚本文件,它即可使用上面的变量,分别在complieopen_url函数中予以调用以完成特定的工作。

config函数中,我们可在此集中设置子项目相关的属性,如C源代码的文件名称,HTML模板名称,以及最终输出的网页名称等等。将此函数作列在最顶端,可方便我们快速而清晰地为不同子项目设置相应的属性。

完毕。

一键运行NetBeansOutput窗口显示:

1. Setting environment variables... 2. Setting variables for development... 3. Compiling... 4. Open browser to visit the url... Done.

现在,只需一个傻瓜式的emcc-compile按钮,足以应对众多不同的子项目。

本节小结

多年以前,我使用DocBook写作,也是利用了类似原理的技术,只是通过Ant来实现,所生成的HTMLPDF既快又美观。以后将以DocBook专题予以分享。

因此,充分挖掘我们手边工具的潜力,可让生产效率成倍以上提高。虽然在攻关时总会花费一定的时间,但一旦成功,则我们以后将受益匪浅。

参考资源

Introduction

  1. webassembly.org

Shell Scripting

  1. Shell Scripting Primer (Apple)