Add Native Gateway to JSC
- Dominant language
- Perl
- Stars
- 3.9k
- Forks
- 213
- Avg merge
- 1m
- Merged PRs (30d)
- 1
Description
As discussed in #281, it would be very cool if there was a gateway in the `jsc` command so that scripts run by it could modify the system (files and network).
Note, I've used the "namespace" object of `jscp`, but there are many options (feel free to choose any):
* `jscp` - *JSC*-*P*lus Native
* `jscn` - *JSC*-Plus-*N*ative
* `jsci` - *JSC*-Native-*I*nterface
* `jsc` - Just JSC
## API Reference
| Function Signature | Documentation |
| ------------------ | ------------- |
| `jscp.readFile(filePath: string): string` | Open the file at `filePath` as a UTF-8 file, return the string contents to the JS. |
| `jscp.readFileBytes(filePath: string): number[]` | Open the file at `filePath`, return the contents as an array of bytes (i.e. an array of integers, each in [0, 127]). |
| `jscp.writeFile(filePath: string, content: string): Result` | Writes `content` to a UTF-8 file at `filePath`. |
| `jscp.writeFileBytes(filePath: string, content: number[]): Result` | Writes `content` as bytes into the file at `filePath`. |
| `jscp.listFiles(folderPath: string): string[]` | Returns a list of the file names in the folder at `folderPath`. |
| `jscp.isFile(filePath: string): boolean` | Returns `true` if there is a file at `filePath`, `false` if there is a folder or nothing there. |
| `jscp.isFolder(folderPath: string): boolean` | Returns `true` if there is a folder at `folderPath`, `false` if there is a file or nothing there. |
| `jscp.getFileSize(filePath: string): number` | Gets the file size of the file at `filePath`. |
| `jscp.makeFolder(folderPath: string): Result` | Creates a folder at `folderPath`. |
| `jscp.move(pathA: string, pathB: string): Result` | Moves a file from `pathA` to `pathB`. |
| `jscp.copy(pathA: string, pathB: string): Result` | Creates a copy of the file at `pathA` and puts it at `pathB`. |
| `jscp.delete(filePath: string): Result` | Deletes the file at `filePath`. |
| `jscp.request(request: Request): Promise` | runs an HTTP request. The input `Request` would specify the verb (`GET`/`POST`), HTTP Headers, and a body as either a string and/or bytes. The output `Response` would contain a code, headers, and body as either a string and/or byte array. |
| `jscp.setTimeout(callback: function, time: number): number` | Runs `callback()` after `time` milliseconds. Returns an ID value. |
| `jscp.clearTimeout(id: number)` | Given an ID from the previous function, stops that timeout from triggering. |
| `jscp.done()` | The JS calls this when it's done running. Not needed if we track all of the asynchronous calls (i.e. `request` promises). |
| `jscp.importModule(filePath: string): any` | Runs the JS file at `filePath` as a new JSC Context in the same JSC Virtual Machine. Returns the result of the new context's `module.exports` variable. |
## Implementation Notes
Here's some tips I've collected in my research on feasibility.
### General
To create a gateway between JSC and Swift functions, check out [my demo here](https://github.com/Recognition101/wasmer-jsc-demo). The `jsc.swift` file demonstrates how to have the JS function `jscp.done` call into the swift `isDone` function.
### Set / Clear Timeout
This function will need to save a callback and call it after some delay. The callback will need to be saved somewhere so that the clear function can cancel it. Documentation for calling JS functions passed in is [here](https://developer.apple.com/documentation/javascriptcore/jsvalue/1451648-call). There are some good implementations [on this StackOverflow response](https://stackoverflow.com/questions/15991044/ios-implemention-of-window-settimeout-with-javascriptcore).
### File Function Errors
All of the file-system functions (i.e: `readFile` to `delete` in the above list) may fail in the case of file not found / permissions / etc. In this case, we can create an error with [this function](https://developer.apple.com/documentation/javascriptcore/jsvalue/1451630-init) and throw that error by setting the JSContext's [exception](https://developer.apple.com/documentation/javascriptcore/jscontext/1451499-exception) property.
### Request
This one is tricky, because it returns a `Promise`. A `Promise` can be created with [this function](https://developer.apple.com/documentation/javascriptcore/jsvalue/3335012-init). The way promises work in JS is the `callback` is immediately called, at which point we start the network request. The two arguments `callback` is called with (as per the previously linked documentation) are, respectively, a "resolve" and "reject" callback (each `JSValue` argument is, itself, a function). When the network request completes successfully, Swift will need to call the first function ("resolve") with the argument to be passed back to the JS. If the network request fails, instead call the second function with an `Error` JSValue (which can be created [with this function](https://developer.apple.com/documentation/javascriptcore/jsvalue/1451630-init)).
### Asynchronous Functions (Timeout and Request) / "Done" Function
All operations in JSC are synchronous. However, in adding support for the native Timeout / Request functions, asynchronicity will have been introduced. At that point, there will need to be logic to keep the thread alive until all timeouts / requests have been satisfied. Only then, after executing the last JS Callback (the callback function of a Timeout or the "resolve"/"reject" function of a Promise), can we be sure the process is complete (ending the command).
### Import Module Function
This is probably the most involved function. As a prerequisite, all JSContexts need to be initialized with a `module` object, by executing the line `global.module = { };`, similar to the `global.jscp = { }` line used to set up the gateway functions.
The import function needs to get the main JSContext's [VirtualMachine](https://developer.apple.com/documentation/javascriptcore/jscontext/1451510-virtualmachine) property and use that to instantiate a *new* JSContext by passing the VM into the [init function](https://developer.apple.com/documentation/javascriptcore/jscontext/1451554-init). Run the new file in the new JSContext, and then extract the value of `module.exports`, returning that `JSValue` as the return value of `importModule`.
Note: The results of this function need to be cached, so that if we `importModule` a specific file multiple times, the same `JSValue` should always be returned. This is for both performance and statefulness reasons (ex: if one part of the code runs `importModule("foo.js").bar = 100` then `console.log(importModule("foo.js").bar)` should log `100`, even if `foo.js` did not set `module.exports.bar` itself).
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.