Add loadable examples support, and add the pong example from the official wiki. Make compiler flags configurable. Put examples in separate files. Change max. delay for waiting for output from 2 sec to 10 secs.master
@@ -0,0 +1,4 @@ | |||
<?xml version="1.0" encoding="UTF-8"?> | |||
<project version="4"> | |||
<component name="Encoding" addBOMForNewFiles="with NO BOM" /> | |||
</project> |
@@ -18,6 +18,10 @@ build: | |||
dev: | |||
npm run dev | |||
.PHONY: api_dev | |||
api_dev: | |||
cd docker_api && bash run_dev.sh | |||
.PHONY: docker_api | |||
docker_api: | |||
cd docker_api && bash build.sh |
@@ -1,5 +1,7 @@ | |||
import binascii | |||
import os | |||
import exec_helpers | |||
import re | |||
from flask import request, url_for | |||
from flask_api import FlaskAPI, status, exceptions | |||
@@ -23,10 +25,22 @@ def compile(): | |||
stderr = '' | |||
retcode = 0 | |||
output = '' | |||
output_wasm = '' | |||
flags = flags.replace("\n", " ") | |||
wasm = 'cheerp-mode=wasm' in flags | |||
# Simple way to disable malicious flags (hopefully) | |||
if not re.match('^[\s\ta-zA-Z0-9=\-_\.]*$', flags): | |||
flags = "" | |||
cmd = "/opt/cheerp/bin/clang {} /tmp/test.cpp -o /tmp/test.js".format(flags) | |||
if wasm: | |||
cmd = "/opt/cheerp/bin/clang {} /tmp/test.cpp -cheerp-wasm-loader=/tmp/test.js -o /tmp/test.wasm".format(flags) | |||
with exec_helpers.Subprocess() as executor: | |||
try: | |||
ret = executor.check_call(cmd, shell=True, timeout=2) | |||
ret = executor.check_call(cmd, shell=True, timeout=10) | |||
if ret: | |||
retcode = ret.exit_code | |||
output = ret.stdout_str | |||
@@ -43,12 +57,18 @@ def compile(): | |||
output = f.read() | |||
f.close() | |||
if wasm and os.path.exists("/tmp/test.wasm"): | |||
with open("/tmp/test.wasm", "rb") as f: | |||
output_wasm = f.read() | |||
f.close() | |||
return { | |||
'stdout': stdout, | |||
'stderr': stderr, | |||
'command': cmd, | |||
'retcode': retcode, | |||
'javascript': output | |||
'javascript': output, | |||
'wasm': binascii.b2a_base64(output_wasm).decode("utf-8") if wasm else "" | |||
}, status.HTTP_201_CREATED | |||
if __name__ == "__main__": |
@@ -0,0 +1,3 @@ | |||
#!/bin/bash | |||
docker run -p 5000:5000 -it cheerp:latest |
@@ -24,13 +24,11 @@ | |||
<a class="navbar-link"> | |||
Load example | |||
</a> | |||
<div class="navbar-dropdown"> | |||
<a class="navbar-item">Example 1 (TODO)</a> | |||
<a class="navbar-item">Example 2 (TODO)</a> | |||
<a class="navbar-item">Example 3 (TODO)</a> | |||
<hr class="navbar-divider"> | |||
<a class="navbar-item">Example 4 (TODO)</a> | |||
</div> | |||
<div class="navbar-dropdown"> | |||
<div v-for="item in examples"> | |||
<a class="navbar-item" v-on:click="load_example(item)">{{ item.title }}</a> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -63,7 +61,9 @@ | |||
<editor-component v-if="editor_tabs == 1" v-model="cpp_code" name="cpp" language="cpp" height="50vh"/> | |||
<div v-if="editor_tabs == 2" class="settings">SETTINGS HERE...</div> | |||
<div v-if="editor_tabs == 2" class="settings"> | |||
<textarea class="textarea" v-model="compiler_flags"></textarea> | |||
</div> | |||
</div> | |||
<div class="row"> | |||
@@ -83,10 +83,12 @@ | |||
<div class="row"> | |||
<div class="tabs"> | |||
<ul> | |||
<li class="is-active"><a>Javascript</a></li> | |||
<li v-bind:class="{'is-active': js_tabs == 1}" v-on:click="js_tabs = 1"><a>Javascript</a></li> | |||
<li v-bind:class="{'is-active': js_tabs == 2}" v-on:click="js_tabs = 2" v-if="wasm_code"><a>Webassembly</a></li> | |||
</ul> | |||
</div> | |||
<editor-component v-model="js_code" name="js" language="javascript" height="50vh"/> | |||
<editor-component v-if="js_tabs == 1" v-model="js_code" name="js" language="javascript" height="50vh"/> | |||
<editor-component v-if="js_tabs == 2 && wasm_code" v-model="wasm_code" name="wasm" language="javascript" height="80vh"/> | |||
</div> | |||
<div class="row"> | |||
<div class="tabs"> | |||
@@ -112,85 +114,14 @@ | |||
const _ = require('lodash'); | |||
const axios = require('axios'); | |||
const cpp_code = ` | |||
#include <cheerp/client.h> | |||
#include <cheerp/clientlib.h> | |||
// We need to extend the client namespace to declare our | |||
// custom JavaScript function | |||
namespace client | |||
{ | |||
// The name should be the same as the JavaScript one | |||
// The parameters needs to be a const client::String reference | |||
// so that implicit conversion from const char* is supported | |||
void changeTitle(const String& str); | |||
} | |||
import { dom_example } from './dom_example' | |||
import { pong_example } from './pong_example' | |||
using namespace client; | |||
void webMain() | |||
{ | |||
Element* titleElement=document.getElementById("pagetitle"); | |||
String* oldText=titleElement->get_textContent(); | |||
changeTitle("Literal C++ string"); | |||
}`.trim(); | |||
const html_code = ` | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>Cheerp test</title> | |||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"><\/script> | |||
<script> | |||
// Use jQuery to make a (trivial) change to the page | |||
function changeTitle(str) | |||
{ | |||
$("#pagetitle").text(str); | |||
} | |||
<\/script> | |||
</head> | |||
<body> | |||
<h1 id="pagetitle">Boring static text</h1> | |||
</body> | |||
</html>`.trim(); | |||
const js_code = ` | |||
"use strict"; | |||
/*Compiled using Cheerp (R) by Leaning Technologies Ltd*/ | |||
var aSlot=null;var oSlot=0;var nullArray=[null];var nullObj={d:nullArray,o:0}; | |||
function __Z7webMainv(){ | |||
var LretConstructor=null; | |||
LretConstructor="pagetitle"; | |||
LretConstructor=document.getElementById(LretConstructor); | |||
LretConstructor.textContent; | |||
LretConstructor="Literal C++ string"; | |||
changeTitle(LretConstructor); | |||
return; | |||
} | |||
function _cheerpCreate_ZN6client6StringC2EPKc(Larg0,Marg0){ | |||
var tmp0=0,Lgeptoindexphi=0,LretConstructor$pi=null,tmp3=null; | |||
LretConstructor$pi=String(); | |||
tmp0=Larg0[Marg0]|0; | |||
if((tmp0&255)===0){ | |||
return String(LretConstructor$pi); | |||
}else{ | |||
Lgeptoindexphi=0; | |||
} | |||
while(1){ | |||
tmp3=String.fromCharCode(tmp0<<24>>24); | |||
LretConstructor$pi=LretConstructor$pi.concat(tmp3); | |||
Lgeptoindexphi=Lgeptoindexphi+1|0; | |||
tmp0=Larg0[Marg0+Lgeptoindexphi|0]|0; | |||
if((tmp0&255)===0){ | |||
break; | |||
} | |||
} | |||
return String(LretConstructor$pi); | |||
} | |||
var _$pstr=new Uint8Array([112,97,103,101,116,105,116,108,101,0]); | |||
var _$pstr1=new Uint8Array([76,105,116,101,114,97,108,32,67,43,43,32,115,116,114,105,110,103,0]); | |||
__Z7webMainv();`.trim(); | |||
let cpp_code = dom_example.cpp | |||
let js_code = dom_example.js | |||
let wasm_code = dom_example.wasm | |||
let html_code = dom_example.html | |||
let compiler_flags = dom_example.flags | |||
function findIframeByName(name) { | |||
return _.find(window.frames, frame => frame.name === name); | |||
@@ -206,6 +137,10 @@ __Z7webMainv();`.trim(); | |||
type: Number, | |||
default: 1, | |||
}, | |||
js_tabs: { | |||
type: Number, | |||
default: 1, | |||
}, | |||
cpp_code: { | |||
type: String, | |||
default: cpp_code, | |||
@@ -217,18 +152,31 @@ __Z7webMainv();`.trim(); | |||
js_code: { | |||
type: String, | |||
}, | |||
wasm_code: { | |||
type: String, | |||
}, | |||
compiler_output: { | |||
type: String, | |||
}, | |||
compiler_flags: { | |||
type: String, | |||
default: '-cheerp-no-type-optimizer -cheerp-pretty-code -cheerp-no-native-math -cheerp-no-math-imul -cheerp-no-math-fround -O3 -target cheerp' | |||
default: compiler_flags | |||
}, | |||
do_update_iframe: { | |||
type: Boolean, | |||
default: false | |||
} | |||
}, | |||
examples: { | |||
type: Array, | |||
default: function () { | |||
return [ | |||
{ title: 'DOM example', cpp_code: dom_example.cpp, 'js_code': dom_example.js, 'wasm_code': dom_example.wasm, 'html_code': dom_example.html, flags: dom_example.flags }, | |||
{ title: 'Pong WASM example', cpp_code: pong_example.cpp, 'js_code': pong_example.js, 'wasm_code': pong_example.wasm, 'html_code': pong_example.html, flags: pong_example.flags }, | |||
] | |||
} | |||
}, | |||
}, | |||
components: { | |||
EditorComponent | |||
@@ -239,6 +187,22 @@ __Z7webMainv();`.trim(); | |||
iframe.document.body.innerHTML = ''; | |||
iframe.document.write(this.html_code); | |||
iframe.document.write("<script>"); | |||
// wasm | |||
iframe.document.write("function fetchBuffer(path) {\n"); | |||
iframe.document.write(" return new Promise( (resolve, reject) => {\n"); | |||
iframe.document.write(" var wasm = '" + this.wasm_code.trim() + "';\n"); | |||
iframe.document.write(" wasm = atob(wasm)\n"); | |||
iframe.document.write("\n"); | |||
iframe.document.write(" var len = wasm.length;\n"); | |||
iframe.document.write(" var bytes = new Uint8Array(len);\n"); | |||
iframe.document.write(" for (var i = 0; i < len; i++) {\n"); | |||
iframe.document.write(" bytes[i] = wasm.charCodeAt(i);\n"); | |||
iframe.document.write(" }\n"); | |||
iframe.document.write(" resolve(bytes.buffer);\n"); | |||
iframe.document.write(" });\n"); | |||
iframe.document.write("}\n"); | |||
iframe.document.write(this.js_code); | |||
iframe.document.write("<\/script>"); | |||
}, | |||
@@ -252,7 +216,7 @@ __Z7webMainv();`.trim(); | |||
}, | |||
compile() { | |||
this.output_tabs = 1; | |||
// axios.post('//localhost:5000/compile', { | |||
//axios.post('//localhost:5000/compile', { | |||
axios.post('https://cheerp.cppse.nl/api/compile', { | |||
flags: this.compiler_flags, | |||
source: this.cpp_code | |||
@@ -266,7 +230,26 @@ __Z7webMainv();`.trim(); | |||
'STDERR:\n------------------------------\n' + | |||
response.data.stderr; | |||
this.compiler_output = str; | |||
this.js_code = response.data.javascript; | |||
var js = response.data.javascript; | |||
var idx = js.indexOf('function fetchBuffer'), jdx = -1; | |||
if (idx != -1) { | |||
var level = 0; | |||
for (var i=idx; ; i++) { | |||
if (js[i] == '{') { | |||
level++; | |||
} | |||
else if (js[i] == '}') { | |||
level--; | |||
if (!level) { | |||
jdx = i; | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
this.js_code = js.substr(0, idx) + js.substr(jdx + 1); | |||
this.wasm_code = response.data.wasm; | |||
}.bind(this)) | |||
.catch(function (error) { | |||
console.log(error); | |||
@@ -280,6 +263,14 @@ __Z7webMainv();`.trim(); | |||
} | |||
catch (e) {} | |||
}, | |||
load_example(ex) { | |||
this.cpp_code = ex.cpp_code | |||
this.js_code = ex.js_code | |||
this.wasm_code = ex.wasm_code | |||
this.html_code = ex.html_code | |||
this.compiler_flags = ex.flags | |||
return true; | |||
} | |||
}, | |||
watch: { | |||
html_code(new_val) { |
@@ -0,0 +1,57 @@ | |||
const cpp_code = ` | |||
#include <cheerp/client.h> | |||
#include <cheerp/clientlib.h> | |||
// We need to extend the client namespace to declare our | |||
// custom JavaScript function | |||
namespace client | |||
{ | |||
// The name should be the same as the JavaScript one | |||
// The parameters needs to be a const client::String reference | |||
// so that implicit conversion from const char* is supported | |||
void changeTitle(const String& str); | |||
} | |||
using namespace client; | |||
void webMain() | |||
{ | |||
Element* titleElement=document.getElementById("pagetitle"); | |||
String* oldText=titleElement->get_textContent(); | |||
changeTitle("Literal C++ string"); | |||
}`.trim(); | |||
const html_code = ` | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>Cheerp test</title> | |||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"><\/script> | |||
<script> | |||
// Use jQuery to make a (trivial) change to the page | |||
function changeTitle(str) | |||
{ | |||
$("#pagetitle").text(str); | |||
} | |||
<\/script> | |||
</head> | |||
<body> | |||
<h1 id="pagetitle">Boring static text</h1> | |||
</body> | |||
</html>`.trim(); | |||
const js_code = ``.trim(); | |||
const flags = ` | |||
-cheerp-pretty-code | |||
-cheerp-no-type-optimizer | |||
-cheerp-no-native-math | |||
-cheerp-no-math-imul | |||
-cheerp-no-math-fround | |||
-O3 | |||
-target cheerp`.trim() | |||
const wasm_code = ''; | |||
export const dom_example = { cpp: cpp_code, js: js_code, wasm: wasm_code, html: html_code, flags: flags } |
@@ -0,0 +1,242 @@ | |||
const cpp_code = ` | |||
#include <cheerp/clientlib.h> | |||
#include <cheerp/client.h> | |||
#include <math.h> | |||
static constexpr int width = 400; | |||
static constexpr int height = 320; | |||
// Forward declaration for the main loop of the game, compiled to WebAssembly | |||
void mainLoop(); | |||
// All the graphics code should stay on the JS side. It is possible to tag whole classes with the [[cheerp::genericjs]] tag. | |||
// All members and methods of this class will be compiled to standard JavaScript. | |||
class [[cheerp::genericjs]] Graphics | |||
{ | |||
private: | |||
// When compiling to standard JavaScript it is possible to use DOM objects like any other C++ object. | |||
static client::HTMLCanvasElement* canvas; | |||
static client::CanvasRenderingContext2D* canvasCtx; | |||
static int width; | |||
static int height; | |||
// This method is the handler for requestAnimationFrame. The browser will call this | |||
// in sync with its graphics loop, usually at 60 fps. | |||
static void rafHandler() | |||
{ | |||
mainLoop(); | |||
client::requestAnimationFrame(cheerp::Callback(rafHandler)); | |||
} | |||
public: | |||
// Define this method later on, we need to declare the Platform class first | |||
static void keyDownHandler(client::KeyboardEvent* e); | |||
static void keyUpHandler(client::KeyboardEvent* e); | |||
static void initializeCanvas(int w, int h) | |||
{ | |||
width = w; | |||
height = h; | |||
canvas = (client::HTMLCanvasElement*)client::document.getElementById("pongcanvas"); | |||
canvas->set_width(w); | |||
canvas->set_height(h); | |||
client::document.get_body()->appendChild(canvas); | |||
canvasCtx = (client::CanvasRenderingContext2D*)canvas->getContext("2d"); | |||
client::requestAnimationFrame(cheerp::Callback(rafHandler)); | |||
// Listen for keydown events | |||
client::document.addEventListener("keydown", cheerp::Callback(keyDownHandler)); | |||
client::document.addEventListener("keyup", cheerp::Callback(keyUpHandler)); | |||
} | |||
static void drawRect(int x, int y, int w, int h, int rgb) | |||
{ | |||
int r = rgb&0xff; | |||
int g = (rgb>>8)&0xff; | |||
int b = (rgb>>16)&0xff; | |||
canvasCtx->set_fillStyle(client::String("").concat("rgb(", r, ",", g, ",", b, ")")); | |||
canvasCtx->fillRect(x, y, w, h); | |||
} | |||
static void drawCircle(int x, int y, int radius, int rgb) | |||
{ | |||
int r = rgb&0xff; | |||
int g = (rgb>>8)&0xff; | |||
int b = (rgb>>16)&0xff; | |||
canvasCtx->set_fillStyle(client::String("").concat("rgb(", r, ",", g, ",", b, ")")); | |||
canvasCtx->beginPath(); | |||
canvasCtx->arc(x,y,radius,0,2*M_PI); | |||
canvasCtx->fill(); | |||
} | |||
static void debugOutput(const char* str) | |||
{ | |||
canvasCtx->set_font("24px sans-serif"); | |||
canvasCtx->set_fillStyle("rgb(255,255,255)"); | |||
canvasCtx->fillText(str, 0, height - 24); | |||
} | |||
}; | |||
// This whole class will be compiled to Wasm code by default since we are using the -cheerp-mode=wasm | |||
// command line option. This is a game entity so it's better to get as much performance as we can. | |||
class Platform | |||
{ | |||
private: | |||
bool moveLeft; | |||
bool moveRight; | |||
int x; | |||
int y; | |||
int width; | |||
int height; | |||
public: | |||
Platform(int x, int y, int width, int height):moveLeft(false),moveRight(false),x(x),y(y),width(width),height(height) | |||
{ | |||
} | |||
int getX() const | |||
{ | |||
return x; | |||
} | |||
int getY() const | |||
{ | |||
return y; | |||
} | |||
int getWidth() const | |||
{ | |||
return width; | |||
} | |||
int getHeight() const | |||
{ | |||
return width; | |||
} | |||
void render() const | |||
{ | |||
Graphics::drawRect(x, y, width, height, 0xffffff); | |||
} | |||
void setMoveLeft(bool val) | |||
{ | |||
moveLeft = val; | |||
} | |||
void setMoveRight(bool val) | |||
{ | |||
moveRight = val; | |||
} | |||
void update() | |||
{ | |||
if (moveLeft) { | |||
x -= 5; | |||
} | |||
if (moveRight) { | |||
x += 5; | |||
} | |||
} | |||
}; | |||
class Ball | |||
{ | |||
private: | |||
int x; | |||
int y; | |||
int vx; | |||
int vy; | |||
public: | |||
Ball(int x, int y, int vx, int vy):x(x),y(y),vx(vx),vy(vy) | |||
{ | |||
} | |||
void update() | |||
{ | |||
x += vx; | |||
y += vy; | |||
} | |||
// Returns true if the ball gets out of the field | |||
bool collide(const Platform& platform, int maxX, int maxY) | |||
{ | |||
// If we collided with the bottom side, we lost | |||
if(y >= maxY) | |||
return true; | |||
// Check left and right side collisions | |||
if(x <= 0 || x >= maxX) | |||
vx = -vx; | |||
// Check top side collision | |||
if(y <= 0) | |||
vy = -vy; | |||
// Check collision with the top side of the plaform | |||
if(platform.getX() < x && (platform.getX() + platform.getWidth()) > x && | |||
platform.getY() < y && (platform.getY() + platform.getHeight()) > y) | |||
{ | |||
vy = -vy; | |||
} | |||
return false; | |||
} | |||
void render() | |||
{ | |||
Graphics::drawCircle(x, y, 5, 0xffffff); | |||
} | |||
}; | |||
// Define global instances for game entities. A more serious game | |||
// would manage these objects dynamically | |||
Platform platform(width / 2 - 15, height - 20, 30, 7); | |||
Ball ball(width / 2, height / 2, 2, -2); | |||
void Graphics::keyDownHandler(client::KeyboardEvent* e) | |||
{ | |||
if(e->get_keyCode() == 37) | |||
platform.setMoveLeft(true); | |||
else if(e->get_keyCode() == 39) | |||
platform.setMoveRight(true); | |||
} | |||
void Graphics::keyUpHandler(client::KeyboardEvent* e) | |||
{ | |||
if(e->get_keyCode() == 37) | |||
platform.setMoveLeft(false); | |||
else if(e->get_keyCode() == 39) | |||
platform.setMoveRight(false); | |||
} | |||
void mainLoop() | |||
{ | |||
// Reset the background to black | |||
Graphics::drawRect(0, 0, width, height, 0x000000); | |||
// Draw the platform | |||
platform.render(); | |||
platform.update(); | |||
// Update the ball state | |||
ball.update(); | |||
// Check for collisions | |||
bool hasLost = ball.collide(platform, width, height); | |||
if(hasLost) | |||
Graphics::debugOutput("You lost!"); | |||
// Render the ball | |||
ball.render(); | |||
} | |||
// This function is the entry point of the program. Since we will be compiling this with the -cheerp-mode=wasm option, it will | |||
// be compiled to WebAssembly by default. | |||
void webMain() | |||
{ | |||
Graphics::initializeCanvas(width, height); | |||
}`.trim(); | |||
const html_code = ` | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>Cheerp test</title> | |||
</head> | |||
<body> | |||
<canvas id="pongcanvas"></canvas> | |||
</body> | |||
</html> | |||
`.trim(); | |||
const js_code = ``.trim() | |||
const wasm_code = ``.trim(); | |||
const flags = ` | |||
-cheerp-pretty-code | |||
-cheerp-no-type-optimizer | |||
-cheerp-no-native-math | |||
-cheerp-no-math-imul | |||
-cheerp-no-math-fround | |||
-target cheerp | |||
-cheerp-mode=wasm | |||
-O2 | |||
`.trim() | |||
export const pong_example = { cpp: cpp_code, js: js_code, wasm: wasm_code, html: html_code, flags: flags } |