Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Deno Runtime into emacs-ng #33

Merged
merged 43 commits into from Dec 29, 2020

Conversation

DavidDeSimone
Copy link
Member

@DavidDeSimone DavidDeSimone commented Dec 25, 2020

The purpose of this PR is to integrate the Deno Runtime. We do this by integrating Chrome's Javascript engine V8, and the deno_core and deno_runtime libraries. deno is dependent on tokio, so we integrate that as well. This code is a strictly additive layer, it changes no elisp functionality, and should be able to merge upstream patches cleanly.

The high level concept here is to allow full control of emacs-ng via the javascript layer. In order to avoid conflict between the elisp and javascript layers, only one scripting engine will be running at a time. elisp is the "authoritative" layer, in that the javascript will invoke elisp functions to interact with the editor. This is done via a special object in javascript called lisp. An example of it's usage would be:

let buffer = lisp.get_buffer_create("mybuf");

The lisp object uses reflection to invoke the equivalent of (get-buffer-create "mybuf"). In this example, get-buffer-create returns a buffer object. In order to represent this, we use a technique called proxying, in which we give a reference to the elisp object to an internal field of a javascript object. This field is only accessible by the native layer, so it cannot be tampered with in the javascript layer. When javascript calls subsequent functions using buffer, a translation layer will extract the elisp object and invoke it on the function.

Primative data structures will be converted automatically. More complex data structures can be converted to native javascript objects via a call to a special function called json.

let qcname = lisp.intern(":name");
let myList = lisp.list(qcname, 3);
console.log(myList); // prints { nativeProxy : true }
console.log(myList.json()); // prints { name: 3 }, defaulting to the assumption this list is a plist 

It's important to note that once you convert to a native js object via a call to json, your mutations do not affect the lisp layer.

let result = lisp.buffer_string(); 
result += "my addition"; // This operation did not edit any lisp object. 
let myList = lisp.list(qcname, 3);
let jsond = myList.json();
jsond.name = 4; // This did not edit a lisp object. 

We expose the async IO functionality included with deno. Users can fetch data async from their local file system, or the network. They can use that data to interact with the editor. An example would be:

const json = fetch("https://api.github.com/users/denoland")
.then((response) => { return response.json(); });

const txt = Deno.readTextFile("./test.json");

Promise.all([json, text])
    .then((data) => {
    	let buffer = lisp.get_buffer_create('hello');
        const current = lisp.current_buffer();
    	lisp.set_buffer(buffer);
    	lisp.insert(JSON.stringify(data[0]));
    	lisp.insert(data[1]);
        console.log(lisp.buffer_string());
        lisp.set_buffer(current); 
    });

This example assumes you have a json file named test.json in your current directory.

Lambdas are converted automatically between the two languages via proxying. You can use this to set timers, or to add hooks. This example features the lisp.symbols object, which returns a proxy to the quoted key. lisp.setq functions similarly to (setq ....)

lisp.add_hook(lisp.symbols.text_mode_hook, () => {
     lisp.setq(lisp.symbols.foo, "3");
 });

// Or set timer with arguments... 
lisp.run_with_timer(lisp.symbols.t, 1, (a, b) => {
    console.log('hello ' + a);
    lisp.print(b);
    return 3;
}, 3, lisp.make.alist({x: 3}));

The user can also define functions via defun that will call back into javascript. Like in normal defun, DOCSTRING and the INTERACTIVE decl are optional. This example features both docstring and interactive.

lisp.defun("my-function", "This is my cool function", {interactive: true, args: "P\nbbuffer"}, 
           (buff) => console.log("Hello Buffer"));

We also leverage module loading. Javascript can be invoked anonymously via (eval-js "...."), or a module can be loaded via (eval-js-file "./my-file.js"). Loading js as a module enables ES6 module imports to be used:

import { operation } from './ops.js';

const results = operation();
// ...

Though tokio is driving the io operations, execution of javascript is strictly controlled by the lisp layer. This is accomplished via a lisp timer. When the timer ticks, we advance the event loop of javascript one iteration.

While a LispObject is being proxy'd by javascript, we add that lisp object to a rooted lisp data structure, and add a special reference to the proxy in javascript called a WeakRef. We use WeakRefs to manage when javascript objects are no longer used by the javascript runtime and can be removed from our lisp GC root.

When executing javascript, invoking lisp functions may result in lisp throwing errors. For example:

const cons = lisp.cons(); // oh no, no arguments!

In this example, lisp.cons will throw a lisp layer error due to not being provided arguments. That error will be caught by the lisp/javascript intermediate layer, and a javascript exception will be thrown, with the value of (error-message-string). This can be caught at this js layer via a standard try/catch, or using a promise error handler.

try {
    const cons = lisp.cons(); //will throw
} catch (e) {
      // perform fallback. 
}

The user can control what permissions javascript is granted when the runtime is initalized by a call to (js-initalize &REST). The user must call this function prior to javascript being executed for it to take effect. Otherwise, the js environment will initialize with default values.

(js-initialize :allow-net nil :allow-read nil :allow-write nil :allow-run nil :js-tick-rate 0.5)

This example, the user has denied javascript the ability to access the network, the file system, or spawn processes. They have increased the tick rate to 0.5.

In order to communicate errors to lisp, the user can provide an error handler at first initalization:

(js-initialize ..... :js-error-handler 'handler)

If the user does not provide an error handler, the default behavior will be to invoke (error ...) on any uncaught javascript errors.

This PR adds a lot of "helper functionality". Readers are directed to javascript.rs for the javascript related changes and additions.

There is still work to do before this is production ready:
☑️ Basic IO POC functional
☑️ Proper garbage collection tracing for lisp objects ref'd by javascript
☑️ Allow the user to control what permissions they will grant javascript
❌ Better control of lisp scoping via javascript (Pushed for now, need to better develop special forms handling)
☑️ Allow lisp errors invoked in lisp.<function>(...) invocations to not poison the javascript runtime
❌ Proper cleanup of javascript runtime upon user request (Pushed for now)
☑️ Allow user to control javascript event loop tick rate
❌ Allow for usage of "recursive-edit" (Pushed pending coordination with Deno team based on JS runtime questions)
☑️ Fix Promise.reject() called in top level module from poisoning the JS runtime.
☑️ 'setq' and callback functionality for things like 'add-hook'

@yyoncho
Copy link
Member

yyoncho commented Dec 25, 2020

This is really exciting!

We expose the async IO functionality included with deno. Users can fetch data async from their local file system, or the network. They can use that data to interact with the editor. An example would be:

How that works?

}

#[lisp_fn]
pub fn js_tick_event_loop() -> LispObject {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate how this will work? Can you also list what are the performance impacting decisions made in this PR and whether you think they can be avoided?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we initialize the runtime, we set up a timer to call js_tick_event_loop. When that timer fires, we call tick_js!(), which fires execute!(). execute! blocks the current thread (the elisp thread) while javascript is running within the context of the tokio runtime. Once that returns, we will exit our timer callback. This is expected to be a fast operation when no pending javascript is awaiting execution.

@DavidDeSimone
Copy link
Member Author

DavidDeSimone commented Dec 25, 2020

This is really exciting!

We expose the async IO functionality included with deno. Users can fetch data async from their local file system, or the network. They can use that data to interact with the editor. An example would be:

How that works?

To break down the code via comments:

// This line will create a promise for the network fetch operation. "Under the hood" this
// will use tokio to perform the network IO. The actual IO will not block lisp or javascript.
const json = fetch("https://api.github.com/users/denoland")
.then((response) => { return response.json(); });

// Similar to the above, but for file system access 
const txt = Deno.readTextFile("./test.json");

// Promise.all will invoke the callback once both operations have completed. 
Promise.all([json, text])
    .then((data) => {
        // We are now in our callback, which was invoked after our async operations
        // completed. Execution was handed off from lisp to javascript via a call to
        // tick_js!(); from our elisp timer. We are free to call lisp functions. 
    	let buffer = lisp.get_buffer_create('hello');
    	lisp.set_buffer(buffer);
    	lisp.insert(JSON.stringify(data[0]));
    	lisp.insert(data[1]);
    });

@yyoncho
Copy link
Member

yyoncho commented Dec 25, 2020

This is really exciting!

We expose the async IO functionality included with deno. Users can fetch data async from their local file system, or the network. They can use that data to interact with the editor. An example would be:

How that works?

To break down the code via comments:

// This line will create a promise for the network fetch operation. "Under the hood" this
// will use tokio to perform the network IO. The actual IO will not block lisp or javascript.
const json = fetch("https://api.github.com/users/denoland")
.then((response) => { return response.json(); });

// Similar to the above, but for file system access 
const txt = Deno.readTextFile("./test.json");

// Promise.all will invoke the callback once both operations have completed. 
Promise.all([json, text])
    .then((data) => {
        // We are now in our callback, which was invoked after our async operations
        // completed. Execution was handed off from lisp to javascript via a call to
        // tick_js!(); from our elisp timer. We are free to call lisp functions. 
    	let buffer = lisp.get_buffer_create('hello');
    	lisp.set_buffer(buffer);
    	lisp.insert(JSON.stringify(data[0]));
    	lisp.insert(data[1]);
    });

Ah, I got it. I thought that somehow the then with return response.json(); somehow is executed on the initial thread.

@DavidDeSimone
Copy link
Member Author

I added a lot today, I would say I'm feeling pretty happy with this as a v1. Requesting another review @brotzeit @yyoncho

@yyoncho
Copy link
Member

yyoncho commented Dec 28, 2020

I am getting

error[E0583]: file not found for module `data`
  --> src/lib.rs:51:1
   |
51 | mod data;
   | ^^^^^^^^^
   |
   = help: to create the module `data`, create file "src/data.rs"

error[E0412]: cannot find type `Lisp_Objfwd` in module `crate::data`
   --> src/eval_macros.rs:102:63
    |
102 |             static mut o_fwd: crate::hacks::Hack<crate::data::Lisp_Objfwd> =
    |                                                               ^^^^^^^^^^^ not found in `crate::data`
    | 
   ::: src/javascript.rs:603:5
    |
603 |     defvar_lisp!(Vjs_retain_map, "js-retain-map", crate::remacs_sys::Qnil);
    |     ----------------------------------------------------------------------- in this macro invocation
    |
    = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider importing this struct
    |
1   | use crate::remacs_sys::Lisp_Objfwd;
    |

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0412, E0583.
For more information about an error, try `rustc --explain E0412`.
error: could not compile `remacs`

To learn more, run the command again with --verbose.
make[1]: *** [Makefile:697: ../rust_src/target/debug/libremacs_lib.a] Error 101
make[1]: Leaving directory '/home/yyoncho/Sources/emacs-ng/src'
make: *** [Makefile:444: src] Error 2

Compilation exited abnormally with code 2 at Mon Dec 28 11:40:13

@DavidDeSimone
Copy link
Member Author

I am getting

error[E0583]: file not found for module `data`
  --> src/lib.rs:51:1
   |
51 | mod data;
   | ^^^^^^^^^
   |
   = help: to create the module `data`, create file "src/data.rs"

error[E0412]: cannot find type `Lisp_Objfwd` in module `crate::data`
   --> src/eval_macros.rs:102:63
    |
102 |             static mut o_fwd: crate::hacks::Hack<crate::data::Lisp_Objfwd> =
    |                                                               ^^^^^^^^^^^ not found in `crate::data`
    | 
   ::: src/javascript.rs:603:5
    |
603 |     defvar_lisp!(Vjs_retain_map, "js-retain-map", crate::remacs_sys::Qnil);
    |     ----------------------------------------------------------------------- in this macro invocation
    |
    = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider importing this struct
    |
1   | use crate::remacs_sys::Lisp_Objfwd;
    |

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0412, E0583.
For more information about an error, try `rustc --explain E0412`.
error: could not compile `remacs`

To learn more, run the command again with --verbose.
make[1]: *** [Makefile:697: ../rust_src/target/debug/libremacs_lib.a] Error 101
make[1]: Leaving directory '/home/yyoncho/Sources/emacs-ng/src'
make: *** [Makefile:444: src] Error 2

Compilation exited abnormally with code 2 at Mon Dec 28 11:40:13

Should be fixed now.

/// This is all done this way to support indirection for
/// multi-threaded Emacs.
#[macro_export]
macro_rules! defvar_lisp {
Copy link
Member

@harryfei harryfei Dec 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this version of defvar_lisp! has a bug.
I fixed it in

objvar: unsafe { &crate::remacs_sys::globals.$field_name as *const _ as *mut _ },

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed, thank you for the heads up.

@DavidDeSimone DavidDeSimone merged commit 7963648 into emacs-ng:master Dec 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants