Js_of_ocaml和Emscripten

在线编译器

使用了Js_of_ocaml和emscripten这两个工具把Caml Featherweight移植到了浏览器里:http://maskray.me/portfolio/caml-featherweight/

Caml Featherweight分为编译器(OCaml)和解释器(C)两部分,Js_of_ocaml可以把其中的OCaml部分编译成JavaScript,emscripten则可以把C编译成JavaScript,这样就可以在浏览器客户端里实现编译和执行。

Js_of_ocaml

如果使用Arch Linux的话,建议不要使用extra/ocaml-4.02.1-1,最好安装opam并用opam switch 4.02.1编译一个ocaml。因为extra/camlp4-4.02.0+1-1接口与ocaml不一致,而且不提供/usr/lib/ocaml/camlp4/META,findlib找不到这个库,用这个ocaml编译Js_of_ocaml会出错。

Caml Featherweight在编译生成phrase index和链接过程中均需要使用seek_out来定位,而Js_of_ocaml没有实现这一功能,因此需要做些变更:

  • Js_of_ocaml会把安装目录中的runtime.js与字节码转成的JavaScript拼接在一起,运行时里实现了库函数,而其中的seek_out虽改变了channeloffset,而控制输出字符的caml_ml_output函数并没有利用offset信息。

  • Js_of_ocaml得到的JavaScript把OCaml标准输出和标准出错里的数据用console.logconsole.error输出到JavaScript终端里,而我们希望数据能显示在HTML的一个编辑器框里,因此需要进行重定向。方法是修改js_print_stdoutjs_print_stderr的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
--- /home/ray/.opam/4.02.1/lib/js_of_ocaml/runtime.js   2014-12-27 19:02:52.000000000 +0800
+++ /home/ray/tmp/runtime.js 2014-12-27 19:02:10.331759148 +0800
@@ -3890,13 +3890,26 @@
caml_blit_string(buffer,offset,string,0,len);
}
var jsstring = string.toString();
- var id = jsstring.lastIndexOf("\n");
- if(id < 0)
- oc.buffer+=jsstring;
- else {
+ if (oc.fd <= 2) {
+ var id = jsstring.lastIndexOf("\n");
+ if (id < 0)
+ oc.buffer += jsstring;
+ else {
oc.buffer+=jsstring.substr(0,id+1);
caml_ml_flush (oc);
oc.buffer += jsstring.substr(id+1);
+ }
+ } else {
+ var clen = caml_ml_string_length(oc.file.data);
+ if (oc.offset + len <= clen)
+ caml_blit_string(buffer, 0, oc.file.data, oc.offset, len);
+ else {
+ var new_data = caml_create_string(oc.offset+len);
+ caml_blit_string(oc.file.data, 0, new_data, 0, clen);
+ caml_blit_string(buffer, 0, new_data, oc.offset, len);
+ oc.file.data = new_data;
+ }
+ oc.offset += len;
}
return 0;
}
@@ -4313,19 +4326,13 @@
function js_print_stdout(s) {
// Do not output the last \n if present
// as console logging display a newline at the end
- if(s.charCodeAt(s.length - 1) == 10)
- s = s.substr(0,s.length - 1 );
- var v = joo_global_object.console;
- v && v.log && v.log(s);
+ caml_stdout += s;
}
//Provides: js_print_stderr
function js_print_stderr(s) {
// Do not output the last \n if present
// as console logging display a newline at the end
- if(s.charCodeAt(s.length - 1) == 10)
- s = s.substr(0,s.length - 1 );
- var v = joo_global_object.console;
- v && v.error && v.error(s);
+ caml_stdout += s;
}
//# 1 "jslib_js_of_ocaml.js"
// Js_of_ocaml library

修改_tags文件:

<*.ml> or "js_main.byte": package(js_of_ocaml), package(js_of_ocaml.syntax), syntax(camlp4o)

编译并翻译成JavaScript:

1
2
cd src
ocamlbuild -use-ocamlfind js_main.byte && js_of_ocaml --no-inline --pretty js_main.byte -o ../build/js/camlfwc.js

Js_of_ocaml还有些问题,比如Lexing.from_string似乎不能用,浮点数小数点的解析也会出错。

Emscripten

把C/C++编译成JavaScript的项目还有几个,比较成熟的还有Cheerp,但构建似乎不太容易。

Emscripten项目多了一个部件——emscripten-fastcomp,似乎是为LLVM添加了一个新target,但需要编译LLVM,且目前只支持到llvm 3.4,而Arch Linux里的llvm版本号已经是3.5了。Emscripten旧的后端实际上依然可用,git clone https://github.com/kripken/emscripten后无需编译直接可用,但很多库函数如memcmpgetopt等均不支持,因此需要修改解释器去除这部分的依赖。

1
2
export EMCC_FAST_COMPILER=0
~/tmp/emscripten/emcc -D__WORDSIZE=32 -I src/runtime src/runtime/*.c -o build/js/camlfwrun.js -w

前端

仿照http://batsh.org/,使用Bootstrap 3创建了一个页面,主体是http://ace.c9.io/的两个编辑器框,另外放了几个菜单用于加载样例和运行等。

核心部分是下面的LiveScript代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
stdout = ''
stderr = ''
window.caml_stdout = ''
reset = !->
stdout := ''
stderr := ''
window.caml_stdout = ''
window.Module =
noInitialRun: true
noExitRuntime: true
print: (data)!->
stdout += data+'\n'
run = !->
#$.post '/run', source.getValue(), (data)!->
# if data.err?
# output.setValue data.err
# else
# output.setValue data.output
# output.navigateFileStart()
reset()
exefile = compile source.getValue()
if not exefile?
output.setValue window.caml_stdout
else
buf = new Uint8Array exefile.c
FS.writeFile \a.out, buf, encoding: \binary
Module.callMain [\a.out]
if stderr.length > 0
output.setValue stderr
else
output.setValue stdout.replace(/\n$/, '')
output.navigateFileStart()
pretty = !->
reset()
window.pretty source.getValue()
output.setValue window.caml_stdout
output.navigateFileStart()
example = (name)!->
$.get "examples/#{name}.ml", (data)!->
source
..setValue data
..navigateFileStart()
source = ace.edit 'source'
..setTheme 'ace/theme/tomorrow_night'
..setShowPrintMargin false
..getSession().setMode 'ace/mode/ocaml'
..commands.addCommand do
name: 'run'
bindKey: 'Ctrl-Enter'
exec: run
..setOptions do
fontSize: '18px'
output = ace.edit 'output'
..setTheme 'ace/theme/tomorrow_night'
..setShowPrintMargin false
..setReadOnly true
..setHighlightActiveLine false
..setOptions do
fontSize: '18px'
$('#run').on 'click', run
$('#pretty').on 'click', pretty
$('a[data-example]').on 'click', ->
example $(@).data 'example'