Skip to content

Commit b78aa0c

Browse files
authored
Merge pull request #320 from elixirscript/ffi
FFI
2 parents 9f4630d + caa1478 commit b78aa0c

30 files changed

Lines changed: 319 additions & 286 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [Unreleased]
8+
9+
### Added
10+
- ElixirScript now has an FFI layer for interoperability with JavaScript. For more details, see documentation at `ElixirScript.FFI`
11+
12+
### Changed
13+
- Compiler has been completely rewritten. ElixirScript now requires Erlang 20+ and Elixir 1.5+
14+
715
## [0.28.0] - 2017-06-11
816

917
### Added

JavascriptInterop.md

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,35 @@ JS.debugger()
1313

1414
# Getting the type of a value
1515
JS.typeof(my_value)
16-
17-
# Creating a new JavaScript Map
18-
map = JS.new(JS.Map, [])
1916
```
2017

21-
### Accessing Global Objects, Functions, and Properties
22-
23-
In order to interact with JavaScript things in the global scope, append "JS" to them. The global scope corresponds to whatever the global object is in the JavaScript environment you are in. For example, in a browser this would be `window` or `self`:
18+
### Foreign Function Interface
2419

25-
```elixir
26-
# Calling alert
27-
JS.alert("hello")
20+
ElixirScript calls JavaScript modules through a Foreign Function Interface (FFI). A foreign module is defined by creating a new module and adding `use ElixirScript.FFI` to it.
2821

29-
# Calling a method on Object
30-
JS.Object.keys(my_object)
22+
Here is an example of a foreign module for a JSON module
3123

32-
# Creating a new JavaScript Date
33-
JS.new(JS.Date, [])
24+
```elixir
25+
defmodule MyApp.JSON do
26+
use ElixirScript.FFI
3427

35-
# Getting the outer width
36-
JS.outerWidth
28+
foreign stringify(map)
29+
foreign parse(string)
30+
end
3731
```
3832

39-
### JavaScript modules
40-
41-
ElixirScript can use JavaScript modules from the supported modules systems.
42-
In order to do so, you must tell ElixirScript about them upfront.
33+
Foreign modules map to JavaScript files that export functions defined with the `foreign` macro.
34+
ElixirScript expects JavaScript modules to be in the `priv/elixir_script` directory.
35+
These modules are copied to the output directory upon compilation.
4336

44-
If using ElixirScript in a mix project, you can do so inside of the ElixirScript configuration keyword list
37+
For our example, a JavaScript file must be placed at `priv/elixir_script/my_app/json.js`.
4538

46-
```elixir
47-
def project do
48-
[
49-
app: :my_project,
50-
elixir_script: [
51-
format: :es,
52-
js_modules: [
53-
{React, "react"},
54-
{ReactDOM, "react-dom"}
55-
]
56-
]
57-
]
58-
end
59-
```
60-
61-
Interacting with these modules works the same as interacting with an ElixirScript module
62-
63-
```elixir
64-
React.createElement(...)
39+
It looks like this
40+
```javascript
41+
export default {
42+
stringify: JSON.stringify,
43+
parse: JSON.parse
44+
}
6545
```
6646

6747
## JavaScript Calling ElixirScript

lib/elixir_script/beam.ex

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,11 @@ defmodule ElixirScript.Beam do
1010
@spec debug_info(atom) :: {:ok | :error, map | binary}
1111
def debug_info(module)
1212

13-
#Replacing String module with our ElixirScript's version
14-
def debug_info(String) do
15-
case debug_info(ElixirScript.String) do
13+
#Replace some modules with ElixirScript versions
14+
def debug_info(module) when module in [String, Agent] do
15+
case debug_info(Module.concat(ElixirScript, module)) do
1616
{:ok, info} ->
17-
{:ok, Map.put(info, :module, String)}
18-
e ->
19-
e
20-
end
21-
end
22-
23-
#Replacing Agent module with our ElixirScript's version
24-
def debug_info(Agent) do
25-
case debug_info(ElixirScript.Agent) do
26-
{:ok, info} ->
27-
{:ok, Map.put(info, :module, Agent)}
17+
{:ok, Map.put(info, :module, module)}
2818
e ->
2919
e
3020
end

lib/elixir_script/cli.ex

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ defmodule ElixirScript.CLI do
88
help: :boolean,
99
version: :boolean,
1010
watch: :boolean,
11-
format: :string,
12-
js_module: [:string, :keep]
11+
format: :string
1312
]
1413

1514
@aliases [
@@ -43,8 +42,6 @@ defmodule ElixirScript.CLI do
4342
<module> the entry module of your application
4443
4544
options:
46-
--js-module [<identifer>:<path>] A js module used in your code. ex: React:react
47-
Multiple can be defined
4845
-f --format [format] module format of output. options: es (default), common, umd
4946
-o --output [path] places output at the given path.
5047
Can be a directory or filename.
@@ -72,13 +69,9 @@ defmodule ElixirScript.CLI do
7269
def do_process(input, options) do
7370
{watch, options} = Keyword.pop(options, :watch, false)
7471

75-
js_modules = Keyword.get_values(options, :js_module)
76-
|> build_js_modules
77-
7872
compile_opts = [
7973
output: Keyword.get(options, :output, :stdout),
80-
format: String.to_atom(Keyword.get(options, :format, "es")),
81-
js_modules: js_modules,
74+
format: String.to_atom(Keyword.get(options, :format, "es"))
8275
]
8376

8477
input = handle_input(input)
@@ -106,26 +99,4 @@ defmodule ElixirScript.CLI do
10699
|> List.flatten
107100
|> Enum.map(fn(x) -> Module.concat([x]) end)
108101
end
109-
110-
defp build_js_modules(values) do
111-
values
112-
|> Enum.map(fn x ->
113-
[identifier, path] = String.split(x, ":", trim: true)
114-
{ format_identifier(identifier), format_path(path) }
115-
end)
116-
end
117-
118-
defp format_identifier(id) do
119-
id
120-
|> String.split(".")
121-
|> Module.concat
122-
end
123-
124-
125-
defp format_path(path) do
126-
path
127-
|> String.replace("\"", "")
128-
|> String.replace("`", "")
129-
|> String.replace("'", "")
130-
end
131102
end

lib/elixir_script/compiler.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ defmodule ElixirScript.Compiler do
3939
default_options = Map.new
4040
|> Map.put(:output, Keyword.get(opts, :output))
4141
|> Map.put(:format, Keyword.get(opts, :format, :es))
42-
|> Map.put(:js_modules, Keyword.get(opts, :js_modules, []))
4342
|> Map.put(:entry_modules, entry_modules)
4443

4544
options = default_options

lib/elixir_script/ffi.ex

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
defmodule ElixirScript.FFI do
2+
@moduledoc """
3+
The foreign function interface for interacting with JavaScript
4+
5+
To define a foreign module, make a new module and add `use ElixirScript.FFI`. to it
6+
To define foreign functions, use the `foreign` macro.
7+
8+
Here is an example of a foreign module for a JSON module
9+
10+
```elixir
11+
defmodule MyApp.JSON do
12+
use ElixirScript.FFI
13+
14+
foreign stringify(map)
15+
foreign parse(string)
16+
end
17+
```
18+
19+
Foreign modules map to JavaScript files that export functions defined with the `foreign` macro.
20+
ElixirScript expects JavaScript modules to be in the `priv/elixir_script` directory.
21+
These modules are copied to the output directory upon compilation.
22+
23+
For our example, a JavaScript file must be placed at `priv/elixir_script/my_app/json.js`.
24+
25+
It looks like this
26+
```javascript
27+
export default {
28+
stringify: JSON.stringify,
29+
parse: JSON.parse
30+
}
31+
```
32+
"""
33+
34+
defmacro __using__(opts) do
35+
quote do
36+
import ElixirScript.FFI
37+
Module.register_attribute __MODULE__, :__foreign_info__, persist: true
38+
@__foreign_info__ %{
39+
path: Macro.underscore(__MODULE__),
40+
name: Enum.join(Module.split(__MODULE__), "_"),
41+
global: unquote(Keyword.get(opts, :global, false))
42+
}
43+
end
44+
end
45+
46+
@doc """
47+
Defines a JavaScript function to be called from Elixir modules
48+
49+
To define a foreign function, pass the name and arguments to `foreign`
50+
51+
```elixir
52+
foreign my_js_function(arg1, arg2, arg3)
53+
```
54+
"""
55+
defmacro foreign({name, _, args}) do
56+
quote do
57+
def unquote(name)(unquote_splicing(args)), do: nil
58+
end
59+
end
60+
end

lib/elixir_script/lib/agent.ex

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,47 @@ defmodule ElixirScript.Agent do
22
@moduledoc false
33

44
def start(fun, options \\ []) do
5-
pid = JS.new JS.Bootstrap.Core.PID, []
6-
75
name = if Keyword.has_key?(options, :name) do
86
Keyword.get(options, :name)
97
else
108
nil
119
end
1210

13-
ElixirScript.Store.create(pid, fun.(), name)
11+
pid = Bootstrap.Core.Store.create(fun.(), name)
1412
{ :ok, pid }
1513
end
1614

1715
def start_link(fun, options \\ []) do
18-
pid = JS.new JS.Bootstrap.Core.PID, []
19-
2016
name = if Keyword.has_key?(options, :name) do
2117
Keyword.get(options, :name)
2218
else
2319
nil
2420
end
2521

26-
ElixirScript.Store.create(pid, fun.(), name)
22+
pid = Bootstrap.Core.Store.create(fun.(), name)
2723
{ :ok, pid }
2824
end
2925

3026
def stop(agent) do
31-
ElixirScript.Store.remove(agent)
27+
Bootstrap.Core.Store.remove(agent)
3228
:ok
3329
end
3430

3531
def update(agent, fun) do
36-
current_state = ElixirScript.Store.read(agent)
37-
ElixirScript.Store.update(agent, fun.(current_state))
32+
current_state = Bootstrap.Core.Store.read(agent)
33+
Bootstrap.Core.Store.update(agent, fun.(current_state))
3834
:ok
3935
end
4036

4137
def get(agent, fun) do
42-
current_state = ElixirScript.Store.read(agent)
38+
current_state = Bootstrap.Core.Store.read(agent)
4339
fun.(current_state)
4440
end
4541

4642
def get_and_update(agent, fun) do
47-
current_state = ElixirScript.Store.read(agent)
43+
current_state = Bootstrap.Core.Store.read(agent)
4844
{val, new_state} = fun.(current_state)
49-
ElixirScript.Store.update(agent, new_state)
45+
Bootstrap.Core.Store.update(agent, new_state)
5046
val
5147
end
5248

lib/elixir_script/lib/store.ex

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,12 @@
1-
defmodule ElixirScript.Store do
1+
defmodule Bootstrap.Core.Store do
2+
@moduledoc false
3+
use ElixirScript.FFI, global: true
24

3-
defp get_key(key) do
4-
real_key = case JS.__elixirscript_names__.has(key) do
5-
true ->
6-
JS.__elixirscript_names__.get(key)
7-
false ->
8-
key
9-
end
5+
foreign create(value, name \\ nil)
106

11-
case JS.__elixirscript_store__.has(real_key) do
12-
true ->
13-
real_key
14-
false ->
15-
JS.throw JS.new(JS.Error, ["Key Not Found"])
16-
end
17-
end
18-
19-
def create(key, value, name \\ nil) do
20-
if name != nil do
21-
JS.__elixirscript_names__.set(name, key)
22-
end
23-
24-
JS.__elixirscript_store__.set(key, value)
25-
end
26-
27-
def update(key, value) do
28-
real_key = get_key(key)
29-
JS.__elixirscript_store__.set(real_key, value)
30-
end
31-
32-
def read(key) do
33-
real_key = get_key(key)
34-
JS.__elixirscript_store__.get(real_key)
35-
end
36-
37-
def remove(key) do
38-
real_key = get_key(key)
39-
JS.__elixirscript_store__.delete(real_key)
40-
end
7+
foreign update(key, value)
418

9+
foreign read(key)
4210

11+
foreign remove(key)
4312
end

lib/elixir_script/lib/string.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ defmodule ElixirScript.String do
1515
end
1616

1717
def to_float(str) do
18-
JS.parseFloat(str)
18+
:erlang.binary_to_float(str)
1919
end
2020

2121
def to_integer(str) do
22-
JS.parseInt(str, 10)
22+
:erlang.binary_to_integer(str)
2323
end
2424

2525
def to_integer(str, base) do
26-
JS.parseInt(str, base)
26+
:erlang.binary_to_integer(str, base)
2727
end
2828

2929
def upcase(str) do

lib/elixir_script/module_systems/common.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ElixirScript.ModuleSystems.Common do
77

88
imports = js_imports
99
|> Enum.map(fn
10-
{module, path} -> import_module(module, path)
10+
{_module, name, path} -> import_module(name, path)
1111
end)
1212

1313
imports = Enum.uniq(imports ++ module_imports)
@@ -16,8 +16,8 @@ defmodule ElixirScript.ModuleSystems.Common do
1616
imports ++ body ++ export
1717
end
1818

19-
defp import_module(module_name, from) do
20-
js_module_name = ElixirScript.Translate.Identifier.make_namespace_members(module_name)
19+
defp import_module(name, from) do
20+
js_module_name = JS.identifier(name)
2121
do_import_module(js_module_name, from)
2222
end
2323

0 commit comments

Comments
 (0)