-
Notifications
You must be signed in to change notification settings - Fork 73
Integrate Deno Runtime into emacs-ng #33
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
Conversation
This is really exciting!
How that works? |
rust_src/src/javascript.rs
Outdated
} | ||
|
||
#[lisp_fn] | ||
pub fn js_tick_event_loop() -> LispObject { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 |
I am getting
|
Should be fixed now. |
/// This is all done this way to support indirection for | ||
/// multi-threaded Emacs. | ||
#[macro_export] | ||
macro_rules! defvar_lisp { |
There was a problem hiding this comment.
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
emacs-ng/rust_src/src/eval_macros.rs
Line 104 in 6571919
objvar: unsafe { &crate::remacs_sys::globals.$field_name as *const _ as *mut _ }, |
There was a problem hiding this comment.
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.
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: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 usingbuffer
, 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
.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.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:
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 ....)
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.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: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 useWeakRef
s 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:
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.
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.
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:
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'