moved platform-switching

This commit is contained in:
Anton-4 2022-10-17 15:22:20 +02:00
parent dc205eab66
commit 3dad6aba7c
No known key found for this signature in database
GPG key ID: A13F4A6E21141925
34 changed files with 25 additions and 25 deletions

View file

@ -1,7 +0,0 @@
rocLovesC
rocLovesPlatforms
rocLovesRust
rocLovesSwift
rocLovesWebAssembly
rocLovesZig
*.wasm

View file

@ -1,20 +0,0 @@
# Platform switching
To run, `cd` into this directory and run this in your terminal:
```bash
roc run
```
This will run `main.roc` because, unless you explicitly give it a filename, `roc run`
defaults to running a file named `main.roc`. Other `roc` commands (like `roc build`, `roc test`, and so on) also default to `main.roc` unless you explicitly give them a filename.
## About this example
This uses a very simple platform which does nothing more than printing the string you give it.
The line `main = "Which platform am I running on now?\n"` sets this string to be `"Which platform am I running on now?"` with a newline at the end, and the lines `packages { pf: "c-platform/main.roc" }` and `provides [main] to pf` specify that the `c-platform/` directory contains this app's platform.
This platform is called `c-platform` because its lower-level code is written in C. There's also a `rust-platform`, `zig-platform`, and so on; if you like, you can try switching `pf: "c-platform/main.roc"` to `pf: "zig-platform/main.roc"` or `pf: "rust-platform/main.roc"` to try one of those platforms instead. They all do similar things, so the application won't look any different.
If you want to start building your own platforms, these are some very simple example platforms to use as starting points.

View file

@ -1,89 +0,0 @@
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void* roc_alloc(size_t size, unsigned int alignment) { return malloc(size); }
void* roc_realloc(void* ptr, size_t new_size, size_t old_size,
unsigned int alignment) {
return realloc(ptr, new_size);
}
void roc_dealloc(void* ptr, unsigned int alignment) { free(ptr); }
void roc_panic(void* ptr, unsigned int alignment) {
char* msg = (char*)ptr;
fprintf(stderr,
"Application crashed with message\n\n %s\n\nShutting down\n", msg);
exit(0);
}
void* roc_memcpy(void* dest, const void* src, size_t n) {
return memcpy(dest, src, n);
}
void* roc_memset(void* str, int c, size_t n) { return memset(str, c, n); }
struct RocStr {
char* bytes;
size_t len;
size_t capacity;
};
bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; }
// Determine the length of the string, taking into
// account the small string optimization
size_t roc_str_len(struct RocStr str) {
char* bytes = (char*)&str;
char last_byte = bytes[sizeof(str) - 1];
char last_byte_xored = last_byte ^ 0b10000000;
size_t small_len = (size_t)(last_byte_xored);
size_t big_len = str.len;
// Avoid branch misprediction costs by always
// determining both small_len and big_len,
// so this compiles to a cmov instruction.
if (is_small_str(str)) {
return small_len;
} else {
return big_len;
}
}
extern void roc__mainForHost_1_exposed_generic(struct RocStr *string);
int main() {
struct RocStr str;
roc__mainForHost_1_exposed_generic(&str);
// Determine str_len and the str_bytes pointer,
// taking into account the small string optimization.
size_t str_len = roc_str_len(str);
char* str_bytes;
if (is_small_str(str)) {
str_bytes = (char*)&str;
} else {
str_bytes = str.bytes;
}
// Write to stdout
if (write(1, str_bytes, str_len) >= 0) {
// Writing succeeded!
// NOTE: the string is a static string, read from in the binary
// if you make it a heap-allocated string, it'll be leaked here
return 0;
} else {
printf("Error writing to stdout: %s\n", strerror(errno));
// NOTE: the string is a static string, read from in the binary
// if you make it a heap-allocated string, it'll be leaked here
return 1;
}
}

View file

@ -1,9 +0,0 @@
platform "echo-in-c"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main

View file

@ -1,11 +0,0 @@
app "rocLovesPlatforms"
packages { pf: "c-platform/main.roc" }
# To switch platforms, comment-out the line above and un-comment one below.
# packages { pf: "rust-platform/main.roc" }
# packages { pf: "swift-platform/main.roc" }
# packages { pf: "web-assembly-platform/main.roc" } # See ./web-assembly-platform/README.md
# packages { pf: "zig-platform/main.roc" }
imports []
provides [main] to pf
main = "Which platform am I running on now?\n"

View file

@ -1,6 +0,0 @@
app "rocLovesC"
packages { pf: "c-platform/main.roc" }
imports []
provides [main] to pf
main = "Roc <3 C!\n"

View file

@ -1,6 +0,0 @@
app "rocLovesRust"
packages { pf: "rust-platform/main.roc" }
imports []
provides [main] to pf
main = "Roc <3 Rust!\n"

View file

@ -1,6 +0,0 @@
app "rocLovesSwift"
packages { pf: "swift-platform/main.roc" }
imports []
provides [main] to pf
main = "Roc <3 Swift!\n"

View file

@ -1,6 +0,0 @@
app "rocLovesWebAssembly"
packages { pf: "web-assembly-platform/main.roc" }
imports []
provides [main] to pf
main = "Roc <3 Web Assembly!\n"

View file

@ -1,6 +0,0 @@
app "rocLovesZig"
packages { pf: "zig-platform/main.roc" }
imports []
provides [main] to pf
main = "Roc <3 Zig!\n"

View file

@ -1,37 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "host"
version = "0.0.1"
dependencies = [
"libc",
"roc_std",
]
[[package]]
name = "libc"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
[[package]]
name = "roc_std"
version = "0.0.1"
dependencies = [
"arrayvec",
"static_assertions",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"

View file

@ -1,22 +0,0 @@
[package]
name = "host"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
links = "app"
[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib"]
[[bin]]
name = "host"
path = "src/main.rs"
[dependencies]
roc_std = { path = "../../../../crates/roc_std" }
libc = "0.2"
[workspace]

View file

@ -1,4 +0,0 @@
fn main() {
println!("cargo:rustc-link-lib=dylib=app");
println!("cargo:rustc-link-search=.");
}

View file

@ -1,3 +0,0 @@
extern int rust_main();
int main() { return rust_main(); }

View file

@ -1,9 +0,0 @@
platform "echo-in-rust"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main

View file

@ -1,78 +0,0 @@
#![allow(non_snake_case)]
use core::ffi::c_void;
use roc_std::RocStr;
use std::ffi::CStr;
use std::mem::ManuallyDrop;
use std::os::raw::c_char;
extern "C" {
#[link_name = "roc__mainForHost_1_exposed_generic"]
fn roc_main(_: &mut RocStr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void {
libc::memcpy(dst, src, n)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
unsafe {
// ManuallyDrop must be used here in order to prevent the RocStr from
// getting dropped as soon as it's no longer referenced anywhere, which
// happens earlier than the libc::write that receives a pointer to its data.
let mut roc_str = ManuallyDrop::new(RocStr::default());
roc_main(&mut roc_str);
let len = roc_str.len();
let str_bytes = roc_str.as_bytes().as_ptr() as *const libc::c_void;
if libc::write(1, str_bytes, len) < 0 {
panic!("Writing to stdout failed!");
}
ManuallyDrop::drop(&mut roc_str)
}
// Exit code
0
}

View file

@ -1,3 +0,0 @@
fn main() {
std::process::exit(host::rust_main() as _);
}

View file

@ -1,9 +0,0 @@
#include <stdlib.h>
struct RocStr {
char* bytes;
size_t len;
size_t capacity;
};
extern void roc__mainForHost_1_exposed_generic(const struct RocStr *data);

View file

@ -1,62 +0,0 @@
import Foundation
@_cdecl("roc_alloc")
func rocAlloc(size: Int, _alignment: UInt) -> UInt {
guard let ptr = malloc(size) else {
return 0
}
return UInt(bitPattern: ptr)
}
@_cdecl("roc_dealloc")
func rocDealloc(ptr: UInt, _alignment: UInt) {
free(UnsafeMutableRawPointer(bitPattern: ptr))
}
@_cdecl("roc_realloc")
func rocRealloc(ptr: UInt, _oldSize: Int, newSize: Int, _alignment: UInt) -> UInt {
guard let ptr = realloc(UnsafeMutableRawPointer(bitPattern: ptr), newSize) else {
return 0
}
return UInt(bitPattern: ptr)
}
func isSmallString(rocStr: RocStr) -> Bool {
return rocStr.capacity < 0
}
func getStrLen(rocStr: RocStr) -> Int {
if isSmallString(rocStr: rocStr) {
// Small String length is last in the byte of capacity.
var cap = rocStr.capacity
let count = MemoryLayout.size(ofValue: cap)
let bytes = Data(bytes: &cap, count: count)
let lastByte = bytes[count - 1]
return Int(lastByte ^ 0b1000_0000)
} else {
return rocStr.len
}
}
func getSwiftString(rocStr: RocStr) -> String {
let length = getStrLen(rocStr: rocStr)
if isSmallString(rocStr: rocStr) {
let data: Data = withUnsafePointer(to: rocStr) { ptr in
Data(bytes: ptr, count: length)
}
return String(data: data, encoding: .utf8)!
} else {
let data = Data(bytes: rocStr.bytes, count: length)
return String(data: data, encoding: .utf8)!
}
}
@_cdecl("main")
func main() -> UInt8 {
var rocStr = RocStr()
roc__mainForHost_1_exposed_generic(&rocStr)
print(getSwiftString(rocStr: rocStr), terminator: "")
return 0
}

View file

@ -1,9 +0,0 @@
platform "echo-in-swift"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main

View file

@ -1,55 +0,0 @@
# Hello, World!
To run this website, first compile either of these identical apps:
```bash
# Option A: Compile crates/cli_testing_examples/platform-switching/rocLovesWebAssembly.roc
cargo run -- build --target=wasm32 crates/cli_testing_examples/platform-switching/rocLovesWebAssembly.roc
# Option B: Compile crates/cli_testing_examples/platform-switching/main.roc with `pf: "web-assembly-platform/main.roc"` and move the result
cargo run -- build --target=wasm32 crates/cli_testing_examples/platform-switching/main.roc
(cd crates/cli_testing_examples/platform-switching && mv rocLovesPlatforms.wasm web-assembly-platform/rocLovesWebAssembly.wasm)
```
Then `cd` into the website directory
and run any web server that can handle WebAssembly.
For example, with `http-server`:
```bash
cd crates/cli_testing_examples/platform-switching/web-assembly-platform
npm install -g http-server
http-server
```
Now open your browser at <http://localhost:8080>
## Design Notes
This demonstrates the basic design of hosts: Roc code gets compiled into a pure
function (in this case, a thunk that always returns `"Hello, World!\n"`) and
then the host calls that function. Fundamentally, that's the whole idea! The host
might not even have a `main` - it could be a library, a plugin, anything.
Everything else is built on this basic "hosts calling linked pure functions" design.
For example, things get more interesting when the compiled Roc function returns
a `Task` - that is, a tagged union data structure containing function pointers
to callback closures. This lets the Roc pure function describe arbitrary
chainable effects, which the host can interpret to perform I/O as requested by
the Roc program. (The tagged union `Task` would have a variant for each supported
I/O operation.)
In this trivial example, it's very easy to line up the API between the host and
the Roc program. In a more involved host, this would be much trickier - especially
if the API were changing frequently during development.
The idea there is to have a first-class concept of "glue code" which host authors
can write (it would be plain Roc code, but with some extra keywords that aren't
available in normal modules - kinda like `port module` in Elm), and which
describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary.
Roc application authors only care about the Roc-host/Roc-app portion, and the
host author only cares about the Roc-host/C boundary when implementing the host.
Using this glue code, the Roc compiler can generate C header files describing the
boundary. This not only gets us host compatibility with C compilers, but also
Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen)
generates correct Rust FFI bindings from C headers.

View file

@ -1,58 +0,0 @@
async function roc_web_platform_run(wasm_filename, callback) {
const decoder = new TextDecoder();
let memory_bytes;
let exit_code;
function js_display_roc_string(str_bytes, str_len) {
const utf8_bytes = memory_bytes.subarray(str_bytes, str_bytes + str_len);
const js_string = decoder.decode(utf8_bytes);
callback(js_string);
}
const importObj = {
wasi_snapshot_preview1: {
proc_exit: (code) => {
if (code !== 0) {
console.error(`Exited with code ${code}`);
}
exit_code = code;
},
fd_write: (x) => { console.error(`fd_write not supported: ${x}`); }
},
env: {
js_display_roc_string,
roc_panic: (_pointer, _tag_id) => {
throw "Roc panicked!";
},
},
};
let wasm;
const response = await fetch(wasm_filename);
if (WebAssembly.instantiateStreaming) {
// streaming API has better performance if available
wasm = await WebAssembly.instantiateStreaming(response, importObj);
} else {
const module_bytes = await response.arrayBuffer();
wasm = await WebAssembly.instantiate(module_bytes, importObj);
}
memory_bytes = new Uint8Array(wasm.instance.exports.memory.buffer);
try {
wasm.instance.exports._start();
} catch (e) {
const is_ok = e.message === "unreachable" && exit_code === 0;
if (!is_ok) {
console.error(e);
}
}
}
if (typeof module !== "undefined") {
module.exports = {
roc_web_platform_run,
};
}

View file

@ -1,25 +0,0 @@
/**
* Node.js test file for helloWeb example
* We are not running this in CI currently, and Node.js is not a Roc dependency.
* But if you happen to have it, you can run this.
*/
// Node doesn't have the fetch API
const fs = require("fs/promises");
global.fetch = (filename) =>
fs.readFile(filename).then((buffer) => ({
arrayBuffer() {
return buffer;
},
}));
const { roc_web_platform_run } = require("./host");
roc_web_platform_run("./rocLovesWebAssembly.wasm", (string_from_roc) => {
const expected = "Roc <3 Web Assembly!\n";
if (string_from_roc !== expected) {
console.error(`Expected "${expected}", but got "${string_from_roc}"`);
process.exit(1);
}
console.log("OK");
});

View file

@ -1,74 +0,0 @@
const std = @import("std");
const str = @import("str");
const builtin = @import("builtin");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
comptime {
// This is a workaround for https://github.com/ziglang/zig/issues/8218
// which is only necessary on macOS.
//
// Once that issue is fixed, we can undo the changes in
// 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing
// -fcompiler-rt in link.rs instead of doing this. Note that this
// workaround is present in many host.zig files, so make sure to undo
// it everywhere!
if (builtin.os.tag == .macos) {
_ = @import("compiler_rt");
}
}
const Align = extern struct { a: usize, b: usize };
extern fn malloc(size: usize) callconv(.C) ?*align(@alignOf(Align)) anyopaque;
extern fn realloc(c_ptr: [*]align(@alignOf(Align)) u8, size: usize) callconv(.C) ?*anyopaque;
extern fn free(c_ptr: [*]align(@alignOf(Align)) u8) callconv(.C) void;
extern fn memcpy(dest: *anyopaque, src: *anyopaque, count: usize) *anyopaque;
export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque {
_ = alignment;
return malloc(size);
}
export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque {
_ = old_size;
_ = alignment;
return realloc(@alignCast(@alignOf(Align), @ptrCast([*]u8, c_ptr)), new_size);
}
export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void {
_ = alignment;
free(@alignCast(@alignOf(Align), @ptrCast([*]u8, c_ptr)));
}
export fn roc_memcpy(dest: *anyopaque, src: *anyopaque, count: usize) callconv(.C) void {
_ = memcpy(dest, src, count);
}
// NOTE roc_panic is provided in the JS file, so it can throw an exception
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed(*RocStr) void;
const Unit = extern struct {};
extern fn js_display_roc_string(str_bytes: ?[*]u8, str_len: usize) void;
pub fn main() u8 {
// actually call roc to populate the callresult
var callresult = RocStr.empty();
roc__mainForHost_1_exposed(&callresult);
// display the result using JavaScript
js_display_roc_string(callresult.asU8ptr(), callresult.len());
callresult.deinit();
return 0;
}

View file

@ -1,12 +0,0 @@
<html>
<body>
<div id="output"></div>
<script src="./host.js"></script>
<script>
const elem = document.getElementById("output");
roc_web_platform_run("./rocLovesWebAssembly.wasm", (string_from_roc) => {
elem.textContent = string_from_roc;
});
</script>
</body>
</html>

View file

@ -1,9 +0,0 @@
platform "echo-in-web-assembly"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main

View file

@ -1,106 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const str = @import("str");
const RocStr = str.RocStr;
const testing = std.testing;
const expectEqual = testing.expectEqual;
const expect = testing.expect;
comptime {
// This is a workaround for https://github.com/ziglang/zig/issues/8218
// which is only necessary on macOS.
//
// Once that issue is fixed, we can undo the changes in
// 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing
// -fcompiler-rt in link.rs instead of doing this. Note that this
// workaround is present in many host.zig files, so make sure to undo
// it everywhere!
if (builtin.os.tag == .macos) {
_ = @import("compiler_rt");
}
}
const Align = 2 * @alignOf(usize);
extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque;
extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque;
extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void;
extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void;
extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void;
const DEBUG: bool = false;
export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
var ptr = malloc(size);
const stdout = std.io.getStdOut().writer();
stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable;
return ptr;
} else {
return malloc(size);
}
}
export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable;
}
return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size);
}
export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void {
if (DEBUG) {
const stdout = std.io.getStdOut().writer();
stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable;
}
free(@alignCast(Align, @ptrCast([*]u8, c_ptr)));
}
export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void {
_ = tag_id;
const stderr = std.io.getStdErr().writer();
const msg = @ptrCast([*:0]const u8, c_ptr);
stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable;
std.process.exit(0);
}
export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void {
return memcpy(dst, src, size);
}
export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void {
return memset(dst, value, size);
}
const mem = std.mem;
const Allocator = mem.Allocator;
extern fn roc__mainForHost_1_exposed_generic(*RocStr) void;
const Unit = extern struct {};
pub fn main() u8 {
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();
var timer = std.time.Timer.start() catch unreachable;
// actually call roc to populate the callresult
var callresult = RocStr.empty();
roc__mainForHost_1_exposed_generic(&callresult);
const nanos = timer.read();
const seconds = (@intToFloat(f64, nanos) / 1_000_000_000.0);
// stdout the result
stdout.print("{s}", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;
return 0;
}

View file

@ -1,9 +0,0 @@
platform "echo-in-zig"
requires {} { main : Str }
exposes []
packages {}
imports []
provides [mainForHost]
mainForHost : Str
mainForHost = main