https://rust-tutorials.github.io/triangle-from-scratch/web_stuff/web_gl_with_bare_wasm.html
이 강좌는 모든 것을 스스로 하는 스타일의 강좌
"Rust for Wasm" 생태계의 대부분은 wasm-bindgen이라는 crate를 사용
새창을 열고 싶다면 winit 또는 sdl2 등의 것들을 사용
웹브라우저에서 무언가를 보여주려면 wasm-bindgen을 사용
일반적으로 wasm-bindgen을 사용하고 있다고 간주
시작하기 전에 올바른 컴파일러와 도구를 사용할 수 있도록 몇 가지 추가 단계를 수행해야 함
Rust를 설치하는 것 외에도 wasm32-unknown-unknown 대상을 설치해야함
rustup target add wasm32-unknown-unknown
GitHub 리포지토리에서 wasm-opt 도구를 얻을 수 있음
The WebAssembly Binary Toolkit (WABT)에서 The WebAssembly Binary Toolkit (WABT)를 얻을 수 있음
프로그램에서 디버깅 기호 등을 제거하여 크기를 상당히 줄일 수 있음
프로그램을 was로 컴파일한 후에는 이를 표시할 방법 필요
ile:// 주소를 사용하여 브라우저에서 로컬 파일을 열 수는 없음
정적 파일을 제공할 수 있는 로컬 서버를 가동하여 화면에 표시
devserver 이용 가능
cargo install devserver
web_crate 폴더 생성
Cargo.toml 파일 생성
[package]
name = "triangle-from-scratch-web-crate"
version = "0.1.0"
authors = ["Lokathor <zefria@gmail.com>"]
edition = "2018"
license = "Zlib OR Apache-2.0 OR MIT"
wasm 라이브러리를 만들려면 crate 유형이 cdylib가 될 것이라고 Rust에 선언
[lib]
crate-type = ["cdylib"]
릴리스 빌드와 함께 링크 시간 최적화를 수행하기 위해 내용 추가
[profile.release]
lto = "thin"
"프로그램"은 실제로 실행 파일로 빌드되지 않음
웹 페이지의 JavaScript가 로드하여 사용할 C 호환 라이브러리로 빌드
이것은 선택적 lib.rs를 사용하여 main.rs를 작성하는 대신 처음부터 코드의 100%를 lib.rs에 넣음
// lib.rs
#[no_mangle]
pub extern "C" fn start() {
// nothing yet!
}
no_mangle 속성은 Rust가 하는 일반적인 이름 맹글링을 완전히 비활성화
이것은 Rust의 특별한 명명 체계를 모르는 외부 코드에 의해 함수가 호출되는 것을 허용
start() 함수는 extern "C" ABI를 사용한다고 선언해야 함
이것은 JavaScript와 Wasm 간에 통신할 때 올바른 호출 규칙을 제공
JavaScript가 wasm 모듈을 로드할 때 start 함수 호출
일반 프로그램의 메인 기능과 유사
사용자에게 표시하고 wasm을 사용할 웹페이지가 필요
index.html 파일 작성
<html>
<body>
<canvas width="800" height="600" id="my_canvas"></canvas>
</body>
</html>
로컬 서버를 시작하고 페이지로 이동
화면에 빈 페이지가 표시된 이후
index.html body 안에 hello를 입력하고 저장 하면
변경된 내용이 즉시 반영됨
<html>
<body>
hello
<canvas width="800" height="600" id="my_canvas"></canvas>
</body>
</html>
index.html 파일 수정
<html>
<body>
<canvas width="800" height="600" id="my_canvas"></canvas>
<script>
var importObject = {};
const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
.then(results => {
results.instance.exports.start();
});
</script>
</body>
</html>
Mozilla 개발자 네트워크(MDN) 페이지에서 WebAssembly 코드 로드 및 실행 튜토리얼 참조
wasm이 캔버스를 흰색이 아닌 색상으로 변경하는 코드 작성
<html>
<body>
<canvas width="800" height="600" id="my_canvas"></canvas>
<script>
var gl;
var canvas;
function setupCanvas() {
console.log("Setting up the canvas...");
let canvas = document.getElementById("my_canvas");
gl = canvas.getContext("webgl");
if (!gl) {
console.log("Failed to get a WebGL context for the canvas!");
return;
}
}
function clearToBlue() {
gl.clearColor(0.1, 0.1, 0.9, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
var importObject = {
env: {
setupCanvas: setupCanvas,
clearToBlue: clearToBlue,
}
};
const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
.then(results => {
results.instance.exports.start();
});
</script>
</body>
</html>
mod js {
extern "C" {
pub fn setupCanvas();
pub fn clearToBlue();
}
}
#[no_mangle]
pub extern "C" fn start() {
unsafe {
js::setupCanvas();
js::clearToBlue();
}
}
wasm 모듈을 다시 빌드하려면 매번
cargo build --release --target wasm32-unknown-unknown
을 수행해야하는 문제 발생
.cargo/config.toml 파일 생성
[build]
target = "wasm32-unknown-unknown"
rustflags = ["-Zstrip=symbols"]
target 은 빌드 타겟 지정
rustflags의 -z는 unstable flag이므로 nightly 빌드 Rust에서 사용
Stable Rust에서는 wabt 툴킷을 사용해야함
Stripping Symbols은 결과물 크기를 줄여줌
HTML 페이지가 자동으로 다시 로드될 때 wasm을 수동으로 다시 빌드해야 하는 부분을 cargo-watch를 이용하여 자동으로 빌드되도록 수정
cargo install cargo-watch
cargo watch -c -x "build --release"
-c 옵션은 리빌드 될때마다 화면을 초기화
-x 옵션은 cargo-watch가 소스코드 수정을 감지했을때마다 "cargo build --release"를 실행시킴
삼각형을 그리려면 지금 우리가 가지고 있는 것보다 더 많은 wasm/js 상호 작용이 필요
#[no_mangle]
pub extern "C" fn start() {
unsafe {
js::setupCanvas();
let vertex_data = [-0.2_f32, 0.5, 0.0, -0.5, -0.4, 0.0, 0.5, -0.1, 0.0];
let vertex_buffer = js::createBuffer();
js::bindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
js::bufferDataF32(
GL_ARRAY_BUFFER,
vertex_data.as_ptr(),
vertex_data.len(),
GL_STATIC_DRAW,
);
let index_data = [0_u16, 1, 2];
let index_buffer = js::createBuffer();
js::bindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
js::bufferDataU16(
GL_ELEMENT_ARRAY_BUFFER,
index_data.as_ptr(),
index_data.len(),
GL_STATIC_DRAW,
);
let vertex_shader_text = "
attribute vec3 vertex_position;
void main(void) {
gl_Position = vec4(vertex_position, 1.0);
}";
let vertex_shader = js::createShader(GL_VERTEX_SHADER);
js::shaderSource(
vertex_shader,
vertex_shader_text.as_bytes().as_ptr(),
vertex_shader_text.len(),
);
js::compileShader(vertex_shader);
let fragment_shader_text = "
void main() {
gl_FragColor = vec4(1.0, 0.5, 0.313, 1.0);
}";
let fragment_shader = js::createShader(GL_FRAGMENT_SHADER);
js::shaderSource(
fragment_shader,
fragment_shader_text.as_bytes().as_ptr(),
fragment_shader_text.len(),
);
js::compileShader(fragment_shader);
let shader_program = js::createProgram();
js::attachShader(shader_program, vertex_shader);
js::attachShader(shader_program, fragment_shader);
js::linkProgram(shader_program);
js::useProgram(shader_program);
let name = "vertex_position";
let attrib_location = js::getAttribLocation(
shader_program,
name.as_bytes().as_ptr(),
name.len(),
);
assert!(attrib_location != GLuint::MAX);
js::enableVertexAttribArray(attrib_location);
js::vertexAttribPointer(attrib_location, 3, GL_FLOAT, false, 0, 0);
js::clearColor(0.37, 0.31, 0.86, 1.0);
js::clear(GL_COLOR_BUFFER_BIT);
js::drawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);
}
}
body의 script 내용 수정
var gl;
var canvas;
var wasm_memory;
var js_objects = [null];
const decoder = new TextDecoder();
function setupCanvas() {
console.log("Setting up the canvas.");
let canvas = document.getElementById("my_canvas");
gl = canvas.getContext("webgl");
if (!gl) {
console.log("Failed to get a WebGL context for the canvas!");
return;
}
}
function clearToBlue() {
gl.clearColor(0.1, 0.1, 0.9, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
var importObject = {
env: {
setupCanvas: setupCanvas,
attachShader: function (program, shader) {
gl.attachShader(js_objects[program], js_objects[shader]);
},
bindBuffer: function (target, id) {
gl.bindBuffer(target, js_objects[id]);
},
bufferDataF32: function (target, data_ptr, data_length, usage) {
const data = new Float32Array(wasm_memory.buffer, data_ptr, data_length);
gl.bufferData(target, data, usage);
},
bufferDataU16: function (target, data_ptr, data_length, usage) {
const data = new Uint16Array(wasm_memory.buffer, data_ptr, data_length);
gl.bufferData(target, data, usage);
},
clear: function (mask) {
gl.clear(mask)
},
clearColor: function (r, g, b, a) {
gl.clearColor(r, g, b, a);
},
compileShader: function (shader) {
gl.compileShader(js_objects[shader]);
},
createBuffer: function () {
return js_objects.push(gl.createBuffer()) - 1;
},
createProgram: function () {
return js_objects.push(gl.createProgram()) - 1;
},
createShader: function (shader_type) {
return js_objects.push(gl.createShader(shader_type)) - 1;
},
drawElements: function (mode, count, type, offset) {
gl.drawElements(mode, count, type, offset);
},
enableVertexAttribArray: function (index) {
gl.enableVertexAttribArray(index)
},
getAttribLocation: function (program, pointer, length) {
const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
const string = decoder.decode(string_data);
return gl.getAttribLocation(js_objects[program], string);
},
linkProgram: function (program) {
gl.linkProgram(js_objects[program]);
},
shaderSource: function (shader, pointer, length) {
const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
const string = decoder.decode(string_data);
gl.shaderSource(js_objects[shader], string);
},
useProgram: function (program) {
gl.useProgram(js_objects[program]);
},
vertexAttribPointer: function (index, size, type, normalized, stride, offset) {
gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
},
}
};
const mod_path = 'target/wasm32-unknown-unknown/release/web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
.then(results => {
console.log("Wasm instance created.");
// assign the memory to be usable by the other functions
wasm_memory = results.instance.exports.memory;
// start the wasm
results.instance.exports.start();
});
마지막으로 시작 코드에 대해 한 가지 더 변경해야 함
const mod_path = 'target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
.then(results => {
console.log("Wasm instance created.");
// assign the memory to be usable by the other functions
wasm_memory = results.instance.exports.memory;
// start the wasm
results.instance.exports.start();
});
결과를 받은 후, export된 메모리를 wasm_memory 값에 할당
이러면 javascript에서 rust 함수를 호출 할 수 있음