├── test ├── test_helper.exs ├── runtests.jl ├── erjulix_test.exs └── test_basics.jl ├── .formatter.exs ├── docs ├── Project.toml ├── src │ └── index.md ├── make.jl └── Manifest.toml ├── lib └── erjulix.ex ├── .github └── workflows │ ├── TagBot.yml │ ├── CompatHelper.yml │ └── ci.yml ├── src ├── srv.erl.bak ├── utils.jl ├── translate.jl ├── ejx_udp.erl └── Erjulix.jl ├── mix.lock ├── .gitignore ├── mix.exs ├── Project.toml ├── LICENSE ├── Manifest.toml └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Erjulix 2 | using Test, SafeTestsets 3 | 4 | @safetestset "Basics" begin include("test_basics.jl") end 5 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 3 | Erjulix = "de6a4c11-948d-47de-a21c-147f86858631" 4 | -------------------------------------------------------------------------------- /test/erjulix_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErjulixTest do 2 | use ExUnit.Case 3 | doctest Erjulix 4 | 5 | test "greets the world" do 6 | assert Erjulix.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Erjulix 3 | ``` 4 | 5 | # Erjulix 6 | 7 | Connect Erlang, Julia and Elixir 8 | 9 | Documentation for [Erjulix](https://github.com/pbayer/erjulix). 10 | 11 | ```@index 12 | ``` 13 | 14 | ```@autodocs 15 | Modules = [Erjulix] 16 | ``` 17 | -------------------------------------------------------------------------------- /lib/erjulix.ex: -------------------------------------------------------------------------------- 1 | defmodule Erjulix do 2 | @moduledoc """ 3 | Documentation for `Erjulix`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> Erjulix.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | jobs: 8 | TagBot: 9 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: JuliaRegistries/TagBot@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | ssh: ${{ secrets.DOCUMENTER_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | workflow_dispatch: 6 | jobs: 7 | CompatHelper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 16 | run: julia -e 'using CompatHelper; CompatHelper.main()' 17 | -------------------------------------------------------------------------------- /src/srv.erl.bak: -------------------------------------------------------------------------------- 1 | -module(srv). 2 | -export([start_server/0]). 3 | 4 | start_server() -> 5 | spawn(fun() -> loop() end). 6 | 7 | %% The server loop 8 | loop() -> 9 | receive 10 | {Caller, Fport, Msg} -> 11 | spawn(fun() -> par_connect(Caller, Fport, Msg) end), 12 | loop() 13 | end. 14 | 15 | %% parallel connect 16 | par_connect(Caller, Fport, Msg) -> 17 | Value = client(Fport, Msg), 18 | case Value of 19 | timeout -> 20 | Caller ! {error, timeout}; 21 | _ -> 22 | Caller ! {ok, Value} 23 | end. 24 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using Erjulix 2 | using Documenter 3 | 4 | DocMeta.setdocmeta!(Erjulix, :DocTestSetup, :(using Erjulix); recursive=true) 5 | 6 | makedocs(; 7 | modules=[Erjulix], 8 | authors="Paul Bayer", 9 | repo="https://github.com/pbayer/erjulix/blob/{commit}{path}#{line}", 10 | sitename="erjulix", 11 | format=Documenter.HTML(; 12 | prettyurls=get(ENV, "CI", "false") == "true", 13 | canonical="https://pbayer.github.io/erjulix", 14 | assets=String[], 15 | ), 16 | pages=[ 17 | "Home" => "index.md", 18 | ], 19 | ) 20 | 21 | deploydocs(; 22 | repo="github.com/pbayer/erjulix", 23 | ) 24 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "erlzmq": {:hex, :erlzmq_dnif, "4.1.0", "f6fab2a98ca8081b7098e1ef62cd5994c7ab133667e042df26ff5807d6de8134", [:rebar3], [], "hexpm", "241fe589d949b3ba47258f37e5482075d728857e2a50ff52c99dcdfd4d9fd435"}, 3 | "jsx": {:hex, :jsx, "2.10.0", "77760560d6ac2b8c51fd4c980e9e19b784016aa70be354ce746472c33beb0b1c", [:rebar3], [], "hexpm", "9a83e3704807298016968db506f9fad0f027de37546eb838b3ae1064c3a0ad62"}, 4 | "jwerl": {:git, "https://github.com/pbayer/jwerl.git", "ee5bfbfc0292ab007529ec09fb02cbeb69d8c0f9", []}, 5 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | # If you run "mix test --cover", coverage assets end up here. 4 | /cover/ 5 | # The directory Mix downloads your dependencies sources to. 6 | /deps/ 7 | # Ignore .fetch files in case you like to edit your project deps locally. 8 | /.fetch 9 | # If the VM crashes, it generates a dump, let's ignore it too. 10 | erl_crash.dump 11 | # Also ignore archive artifacts (built via "mix archive.build"). 12 | *.ez 13 | # Ignore package tarball (built via "mix hex.build"). 14 | erjulix-*.tar 15 | # Temporary files, for example, from tests. 16 | /tmp/ 17 | *.swp 18 | 19 | *.jl.*.cov 20 | *.jl.cov 21 | *.jl.mem 22 | .DS_Store 23 | /Manifest.toml 24 | /dev/ 25 | docs/build 26 | docs/Manifest.toml 27 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Erjulix.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :erjulix, 7 | version: "0.1.0", 8 | elixir: "~> 1.12", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | {:jwerl, git: "https://github.com/pbayer/jwerl.git"}, 27 | {:erlzmq, "~> 4.1", hex: :erlzmq_dnif} 28 | ] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Erjulix" 2 | uuid = "de6a4c11-948d-47de-a21c-147f86858631" 3 | authors = ["Paul Bayer "] 4 | version = "0.1.0" 5 | 6 | [deps] 7 | Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" 8 | ErlangTerm = "6cf46ab0-e18c-5424-9784-c70c22e5b0c7" 9 | JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 10 | JSONWebTokens = "9b8beb19-0777-58c6-920b-28f749fee4d3" 11 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 12 | Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" 13 | 14 | [compat] 15 | JSON = "0.21" 16 | JSONWebTokens = "1.1" 17 | ErlangTerm = "0.1" 18 | julia = "1.6" 19 | 20 | [extras] 21 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 22 | SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" 23 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 24 | 25 | [targets] 26 | test = ["Test", "Documenter", "SafeTestsets"] 27 | -------------------------------------------------------------------------------- /src/utils.jl: -------------------------------------------------------------------------------- 1 | using Random 2 | 3 | const chars = ['a':'z'; 'A':'Z'; '0':'9'; ['/','+','-','=']] 4 | 5 | "Return a Base64 encoded random passwort of length `len`." 6 | genpasswd(len) = randstring(chars, len) 7 | 8 | "Return an available port number ≥ `start`." 9 | function getPort(start::Integer) 10 | s = UDPSocket() 11 | port = start 12 | while true 13 | bind(s, Sockets.localhost, port) && break 14 | port += 1 15 | port > 65535 && throw(SystemError("no port available")) 16 | end 17 | close(s) 18 | port 19 | end 20 | 21 | "Return the `Sockets.InetAddr` of a socket `sock`." 22 | function getHostPort(sock::UDPSocket) 23 | s = UDPSocket() 24 | p = getPort(1000) 25 | @assert bind(s, Sockets.localhost, p) "No port available" 26 | send(sock, Sockets.localhost, p, serialize(0)) 27 | hp, _ = recvfrom(s) 28 | hp 29 | end 30 | -------------------------------------------------------------------------------- /src/translate.jl: -------------------------------------------------------------------------------- 1 | using ErlangTerm 2 | import JSON, JSONWebTokens 3 | 4 | "Serialize `data`, sha-256 encoded with `key` if it is not empty." 5 | function serializek(data, key::AbstractString) 6 | if isempty(key) 7 | serialize(data) 8 | else 9 | JSONWebTokens.encode( 10 | JSONWebTokens.HS256(key), 11 | data 12 | |> serialize 13 | |> JSON.json 14 | ) |> Vector{UInt8} 15 | end 16 | end 17 | 18 | "Deserialize a `binary`, sha-256 decoded with `key` if it is not empty." 19 | function deserializek(binary::Vector{UInt8}, key::AbstractString) 20 | if isempty(key) 21 | deserialize(binary) 22 | else 23 | j = JSONWebTokens.decode( 24 | JSONWebTokens.HS256(key), 25 | String(binary) 26 | ) 27 | deserialize(convert(Vector{UInt8}, j)) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paul Bayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_basics.jl: -------------------------------------------------------------------------------- 1 | using Test, Sockets, Erjulix, ErlangTerm 2 | 3 | localhost = Sockets.localhost 4 | erl = UDPSocket() # erlang test socket 5 | ep = Erjulix.getPort(1000) 6 | @test bind(erl, localhost, ep) 7 | 8 | pport = Erjulix.getPort(1000) 9 | println("pServer setup") 10 | ps = pServer(pport) 11 | 12 | @test ps.state == :runnable 13 | send(erl, localhost, pport, serialize(:srv)) 14 | hp, pkg = recvfrom(erl) 15 | msg = deserialize(pkg) 16 | @test hp.host == localhost 17 | @test first(msg) == :ok 18 | @test last(msg) == repr(Erjulix._ESM[end]) 19 | println("EvalServer at $(hp.host):$(hp.port)") 20 | 21 | # test eval 22 | println("test :eval") 23 | send(erl, hp.host, hp.port, serialize((:eval, "sum(1:10)"))) 24 | println("msg sent!") 25 | sleep(0.5) 26 | @test Erjulix._ESM[end]._eServer.state == :runnable 27 | println("server ok") 28 | pkg = recv(erl) 29 | println("got message from server") 30 | msg = deserialize(pkg) 31 | @test msg == (:ok, 55) 32 | 33 | # test call 34 | println("test :call") 35 | send(erl, hp.host, hp.port, serialize((:call, :sum, [collect(11:20)]))) 36 | sleep(0.2) 37 | @test Erjulix._ESM[end]._eServer.state == :runnable 38 | pkg = recv(erl) 39 | msg = deserialize(pkg) 40 | @test msg == (:ok, 155) 41 | 42 | # test set 43 | println("test :set") 44 | send(erl, hp.host, hp.port, serialize((:set, :a, collect(21:30)))) 45 | sleep(0.2) 46 | @test Erjulix._ESM[end]._eServer.state == :runnable 47 | pkg = recv(erl) 48 | msg = deserialize(pkg) 49 | @test msg == (:ok, :nil) 50 | @test Erjulix._ESM[end].a == collect(21:30) 51 | 52 | # test exit 53 | println("test :exit") 54 | send(erl, hp.host, hp.port, serialize(:exit)) 55 | sleep(0.5) 56 | @test Erjulix._ESM[end]._eServer.state == :done 57 | 58 | send(erl, localhost, pport, serialize(:exit)) 59 | sleep(0.5) 60 | @test ps.state == :done 61 | -------------------------------------------------------------------------------- /Manifest.toml: -------------------------------------------------------------------------------- 1 | # This file is machine-generated - editing it directly is not advised 2 | 3 | [[Artifacts]] 4 | uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" 5 | 6 | [[Base64]] 7 | uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" 8 | 9 | [[Dates]] 10 | deps = ["Printf"] 11 | uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" 12 | 13 | [[ErlangTerm]] 14 | git-tree-sha1 = "fdee66b9843f7aa370518b274cffddc10e6e7f54" 15 | uuid = "6cf46ab0-e18c-5424-9784-c70c22e5b0c7" 16 | version = "0.1.1" 17 | 18 | [[JSON]] 19 | deps = ["Dates", "Mmap", "Parsers", "Unicode"] 20 | git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" 21 | uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 22 | version = "0.21.1" 23 | 24 | [[JSONWebTokens]] 25 | deps = ["Base64", "JSON", "MbedTLS", "Random", "SHA"] 26 | git-tree-sha1 = "e1dac908e3a2f28c8aa2e5b194b7d45954d024bf" 27 | uuid = "9b8beb19-0777-58c6-920b-28f749fee4d3" 28 | version = "1.1.0" 29 | 30 | [[Libdl]] 31 | uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" 32 | 33 | [[MbedTLS]] 34 | deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] 35 | git-tree-sha1 = "1c38e51c3d08ef2278062ebceade0e46cefc96fe" 36 | uuid = "739be429-bea8-5141-9913-cc70e7f3736d" 37 | version = "1.0.3" 38 | 39 | [[MbedTLS_jll]] 40 | deps = ["Artifacts", "Libdl"] 41 | uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" 42 | 43 | [[Mmap]] 44 | uuid = "a63ad114-7e13-5084-954f-fe012c677804" 45 | 46 | [[Parsers]] 47 | deps = ["Dates"] 48 | git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" 49 | uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" 50 | version = "1.1.0" 51 | 52 | [[Printf]] 53 | deps = ["Unicode"] 54 | uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" 55 | 56 | [[Random]] 57 | deps = ["Serialization"] 58 | uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 59 | 60 | [[SHA]] 61 | uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" 62 | 63 | [[Serialization]] 64 | uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" 65 | 66 | [[Sockets]] 67 | uuid = "6462fe0b-24de-5631-8697-dd941f90decc" 68 | 69 | [[Unicode]] 70 | uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | env: 5 | JULIA_NUM_THREADS: 2 6 | jobs: 7 | test: 8 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | version: 14 | - '1.6' 15 | - 'nightly' 16 | os: 17 | - ubuntu-latest 18 | arch: 19 | - x64 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: julia-actions/setup-julia@v1 23 | with: 24 | version: ${{ matrix.version }} 25 | arch: ${{ matrix.arch }} 26 | - uses: actions/cache@v1 27 | env: 28 | cache-name: cache-artifacts 29 | with: 30 | path: ~/.julia/artifacts 31 | key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} 32 | restore-keys: | 33 | ${{ runner.os }}-test-${{ env.cache-name }}- 34 | ${{ runner.os }}-test- 35 | ${{ runner.os }}- 36 | - uses: julia-actions/julia-buildpkg@v1 37 | - uses: julia-actions/julia-runtest@v1 38 | - uses: julia-actions/julia-processcoverage@v1 39 | - uses: codecov/codecov-action@v1 40 | with: 41 | file: lcov.info 42 | docs: 43 | name: Documentation 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v2 47 | - uses: julia-actions/setup-julia@v1 48 | with: 49 | version: '1' 50 | - run: | 51 | julia --project=docs -e ' 52 | using Pkg 53 | Pkg.develop(PackageSpec(path=pwd())) 54 | Pkg.instantiate()' 55 | - run: | 56 | julia --project=docs -e ' 57 | using Documenter: DocMeta, doctest 58 | using Erjulix 59 | DocMeta.setdocmeta!(Erjulix, :DocTestSetup, :(using Erjulix); recursive=true) 60 | doctest(Erjulix)' 61 | - run: julia --project=docs docs/make.jl 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} 65 | -------------------------------------------------------------------------------- /src/ejx_udp.erl: -------------------------------------------------------------------------------- 1 | -module(ejx_udp). 2 | -export([call/2, call/3, call/4, eval/2, eval/3, set/3, set/4, 3 | client/2, client/3, srv/1]). 4 | 5 | %% @type addr(). Can be 6 | %% - a port on the localhost or 7 | %% - a tuple of an IP address and a port number or 8 | %% - a tuple of an IP address, a port number and a key. 9 | -type addr() :: inet:port_number() 10 | | {inet:ip_address(), inet:port_number()} 11 | | {inet:ip_address(), inet:port_number(), string()}. 12 | 13 | %% A Julia term is either an atom referring to a variable or 14 | %% callable or a string containing a Julia expression. 15 | -type jterm() :: atom() | string(). 16 | 17 | -type atoms() :: atom() | [atom()]. 18 | 19 | -type vals() :: any() | [] | list(). 20 | 21 | -type response() :: {ok, any()} | {error, string()}. 22 | 23 | %% @doc Send a Julia 'Term' with 'Args' to an EvalServer 24 | %% at 'Addr' and receive and return the answer. 25 | %% After 'Timeout' return a 'timeout'. 26 | %% @end 27 | -spec call(addr(), jterm(), vals(), timeout()) -> response(). 28 | call(Addr, Term, Args, Timeout) -> 29 | client(Addr, {call, Term, Args}, Timeout). 30 | 31 | %% @doc Send a Julia 'Term' with 'Args' to an EvalServer 32 | %% at 'Addr' and receive and return the answer. 33 | %% After 5s return a 'timeout'. 34 | %% @end 35 | -spec call(addr(), jterm(), vals()) -> response(). 36 | call(Addr, Term, Args) -> 37 | client(Addr, {call, Term, Args}, 5000). 38 | 39 | %% @doc Send a Julia 'Term' without arguments to an EvalServer 40 | %% at 'Addr' and receive and return the answer. 41 | %% After 5s return a 'timeout'. 42 | %% @end 43 | -spec call(addr(), jterm()) -> response(). 44 | call(Addr, Term) -> 45 | client(Addr, {call, Term, []}, 5000). 46 | 47 | %% @doc Send a Julia 'Term' for evaluation to an EvalServer 48 | %% at 'Addr' and receive and return the answer. 49 | %% After 'Timeout' return a 'timeout'. 50 | %% @end 51 | -spec eval(addr(), jterm(), timeout()) -> response(). 52 | eval(Addr, Term, Timeout) -> 53 | client(Addr, {eval, Term}, Timeout). 54 | 55 | %% @doc Send a Julia 'Term' for evaluation to an EvalServer 56 | %% at 'Addr' and receive and return the answer. 57 | %% After 5s return a 'timeout'. 58 | %% @end 59 | -spec eval(addr(), jterm()) -> response(). 60 | eval(Addr, Term) -> 61 | client(Addr, {eval, Term}, 5000). 62 | 63 | %% @doc Send an atom or a list of atoms to a Julia EvalServer 64 | %% at 'Addr' to set it/them to a value or a list of values and 65 | %% to make it/them available for further computations. 66 | %% @end 67 | -spec set(addr(), atoms(), vals(), timeout()) -> response(). 68 | set(Addr, Atoms, Vals, Timeout) -> 69 | client(Addr, {set, Atoms, Vals}, Timeout). 70 | 71 | -spec set(addr(), atoms(), vals()) -> response(). 72 | set(Addr, Atoms, Vals) -> 73 | client(Addr, {set, Atoms, Vals}, 5000). 74 | 75 | client(Addr, Msg) -> 76 | client(Addr, Msg, 5000). 77 | 78 | client(Port, Msg, Timeout) when is_integer(Port) -> 79 | client({"localhost", Port}, Msg, Timeout); 80 | 81 | client({Host, Port}, Msg, Timeout) -> 82 | client({Host, Port, ""}, Msg, Timeout); 83 | 84 | client({Host, Port, Key}, Msg, Timeout) -> 85 | Bin = term_to_binary_k(Msg, Key), 86 | {ok, Socket} = gen_udp:open(0, [binary]), 87 | ok = gen_udp:send(Socket, Host, Port, Bin), 88 | Value = receive 89 | {udp, Socket, _, _, Recv} -> 90 | binary_to_term_k(Recv, Key) 91 | after Timeout -> 92 | timeout 93 | end, 94 | gen_udp:close(Socket), 95 | Value. 96 | 97 | -spec srv(addr()) -> {ok, addr(), any()}. 98 | srv({Host, Port}) -> 99 | srv({Host, Port, ""}); 100 | 101 | srv({Host, Port, Key}) -> 102 | {ok, Socket} = gen_udp:open(0, [binary]), 103 | ok = gen_udp:send(Socket, Host, Port, term_to_binary_k(srv, Key)), 104 | Value = receive 105 | {udp, Socket, Shost, Sport, Recv} -> 106 | Ret = binary_to_term_k(Recv, Key), 107 | case Ret of 108 | {ok, NKey, Mod} -> {ok, {Shost, Sport, NKey}, Mod}; 109 | {ok, Mod} -> {ok, {Shost, Sport}, Mod} 110 | end 111 | after 5000 -> 112 | timeout 113 | end, 114 | gen_udp:close(Socket), 115 | Value; 116 | 117 | srv(Port) -> 118 | srv({"localhost", Port}). 119 | 120 | term_to_binary_k(T, "") -> 121 | term_to_binary(T); 122 | 123 | term_to_binary_k(T, Key) -> 124 | L = binary_to_list(term_to_binary(T)), 125 | jwerl:sign(L, hs256, Key). 126 | 127 | binary_to_term_k(B, "") -> 128 | binary_to_term(B); 129 | 130 | binary_to_term_k(B, Key) -> 131 | {ok, L} = jwerl:verify(B, hs256, Key), 132 | binary_to_term(list_to_binary(L)). 133 | -------------------------------------------------------------------------------- /docs/Manifest.toml: -------------------------------------------------------------------------------- 1 | # This file is machine-generated - editing it directly is not advised 2 | 3 | [[ArgTools]] 4 | uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" 5 | 6 | [[Artifacts]] 7 | uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" 8 | 9 | [[Base64]] 10 | uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" 11 | 12 | [[Dates]] 13 | deps = ["Printf"] 14 | uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" 15 | 16 | [[DocStringExtensions]] 17 | deps = ["LibGit2", "Markdown", "Pkg", "Test"] 18 | git-tree-sha1 = "9d4f64f79012636741cf01133158a54b24924c32" 19 | uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" 20 | version = "0.8.4" 21 | 22 | [[Documenter]] 23 | deps = ["Base64", "Dates", "DocStringExtensions", "IOCapture", "InteractiveUtils", "JSON", "LibGit2", "Logging", "Markdown", "REPL", "Test", "Unicode"] 24 | git-tree-sha1 = "3ebb967819b284dc1e3c0422229b58a40a255649" 25 | uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 26 | version = "0.26.3" 27 | 28 | [[Downloads]] 29 | deps = ["ArgTools", "LibCURL", "NetworkOptions"] 30 | uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" 31 | 32 | [[Erjulix]] 33 | deps = ["ErlangTerm", "Sockets"] 34 | git-tree-sha1 = "2de4799a246721a9755936d91c14a7dbc85a5f00" 35 | repo-rev = "master" 36 | repo-url = "https://github.com/pbayer/erjulix" 37 | uuid = "de6a4c11-948d-47de-a21c-147f86858631" 38 | version = "0.1.0" 39 | 40 | [[ErlangTerm]] 41 | deps = ["Test"] 42 | git-tree-sha1 = "04a208013514603aa21796de554f82cf93ea020c" 43 | uuid = "6cf46ab0-e18c-5424-9784-c70c22e5b0c7" 44 | version = "0.1.0" 45 | 46 | [[IOCapture]] 47 | deps = ["Logging"] 48 | git-tree-sha1 = "377252859f740c217b936cebcd918a44f9b53b59" 49 | uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" 50 | version = "0.1.1" 51 | 52 | [[InteractiveUtils]] 53 | deps = ["Markdown"] 54 | uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" 55 | 56 | [[JSON]] 57 | deps = ["Dates", "Mmap", "Parsers", "Unicode"] 58 | git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" 59 | uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" 60 | version = "0.21.1" 61 | 62 | [[LibCURL]] 63 | deps = ["LibCURL_jll", "MozillaCACerts_jll"] 64 | uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" 65 | 66 | [[LibCURL_jll]] 67 | deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] 68 | uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" 69 | 70 | [[LibGit2]] 71 | deps = ["Base64", "NetworkOptions", "Printf", "SHA"] 72 | uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" 73 | 74 | [[LibSSH2_jll]] 75 | deps = ["Artifacts", "Libdl", "MbedTLS_jll"] 76 | uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" 77 | 78 | [[Libdl]] 79 | uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" 80 | 81 | [[Logging]] 82 | uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" 83 | 84 | [[Markdown]] 85 | deps = ["Base64"] 86 | uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" 87 | 88 | [[MbedTLS_jll]] 89 | deps = ["Artifacts", "Libdl"] 90 | uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" 91 | 92 | [[Mmap]] 93 | uuid = "a63ad114-7e13-5084-954f-fe012c677804" 94 | 95 | [[MozillaCACerts_jll]] 96 | uuid = "14a3606d-f60d-562e-9121-12d972cd8159" 97 | 98 | [[NetworkOptions]] 99 | uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" 100 | 101 | [[Parsers]] 102 | deps = ["Dates"] 103 | git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" 104 | uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" 105 | version = "1.1.0" 106 | 107 | [[Pkg]] 108 | deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] 109 | uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" 110 | 111 | [[Printf]] 112 | deps = ["Unicode"] 113 | uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" 114 | 115 | [[REPL]] 116 | deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] 117 | uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 118 | 119 | [[Random]] 120 | deps = ["Serialization"] 121 | uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 122 | 123 | [[SHA]] 124 | uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" 125 | 126 | [[Serialization]] 127 | uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" 128 | 129 | [[Sockets]] 130 | uuid = "6462fe0b-24de-5631-8697-dd941f90decc" 131 | 132 | [[TOML]] 133 | deps = ["Dates"] 134 | uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" 135 | 136 | [[Tar]] 137 | deps = ["ArgTools", "SHA"] 138 | uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" 139 | 140 | [[Test]] 141 | deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] 142 | uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 143 | 144 | [[UUIDs]] 145 | deps = ["Random", "SHA"] 146 | uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" 147 | 148 | [[Unicode]] 149 | uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" 150 | 151 | [[Zlib_jll]] 152 | deps = ["Libdl"] 153 | uuid = "83775a58-1f1d-513f-b197-d71354ab007a" 154 | 155 | [[nghttp2_jll]] 156 | deps = ["Artifacts", "Libdl"] 157 | uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" 158 | 159 | [[p7zip_jll]] 160 | deps = ["Artifacts", "Libdl"] 161 | uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" 162 | -------------------------------------------------------------------------------- /src/Erjulix.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Erjulix 3 | 4 | Julia module for communicating with Erlang and Elixir. 5 | """ 6 | module Erjulix 7 | 8 | using ErlangTerm, Sockets 9 | 10 | export eServer, pServer, EvalServer, recv_erl, send_erl 11 | 12 | # Vector of created eServer modules 13 | const _ESM = Module[] 14 | 15 | include("utils.jl") 16 | include("translate.jl") 17 | 18 | """ 19 | ``` 20 | eServer(host::IPAddr, port::Integer, key::AbstractString) 21 | eServer(port::Integer) 22 | ``` 23 | Create a module and spawn an `EvalServer` listening to an UDP `port` 24 | in its namespace and return the module. 25 | """ 26 | function eServer(host::IPAddr, port::Integer, key::AbstractString) 27 | sock = UDPSocket() 28 | if bind(sock, host, port) 29 | mdl = Module(gensym(:esm)) 30 | hp = getHostPort(sock) 31 | println("start EvalServer $host:$(hp.port), $mdl") 32 | t = Threads.@spawn EvalServer(sock, mdl, key) 33 | Core.eval(mdl, :(_socket = $sock)) 34 | Core.eval(mdl, :(_eServer = $t)) 35 | push!(_ESM, mdl) 36 | mdl 37 | else 38 | println(stderr, "port $port not available") 39 | close(sock) 40 | end 41 | end 42 | eServer(port::Integer) = eServer(Sockets.localhost, port, "") 43 | 44 | """ 45 | EvalServer(port::Integer, mod::Module, key::AbstractString) 46 | 47 | An `EvalServer` runs as a task with its own module namespace. 48 | It receives UDP message tuples from Erlang: 49 | 50 | - `(:eval, term)`: where `term` is a `Symbol` or a `String`, 51 | - `(:call, term, args)`: with `term` as above and `args` a `Vector{Any}`, 52 | - `(:set, atoms, vals)`: where `atoms` is a `Symbol` or a vector 53 | of them and `vals` is a value `Any` or a vector of them. 54 | 55 | It then evaluates the messages in its namespace as 56 | 57 | 1. strings to parse or symbols to evaluate, 58 | 2. functions to execute with arguments or 59 | 3. variables to create or to assign values to. 60 | 61 | It sends a result tuple back to the Erlang client. 62 | 63 | The server finishes if it gets an `"exit"` or `:exit` message. 64 | 65 | If `!isempty(key)`, the UDP packages get sha-256 encoded/decoded 66 | with that key as JSON Web tokens. 67 | """ 68 | function EvalServer(sock::UDPSocket, mod::Module, key::AbstractString) 69 | while true 70 | hp, msg = recvfrom(sock) 71 | isexit(msg) && break 72 | val = deserializek(msg, key) 73 | # println(@show val) 74 | if val == :exit 75 | send(sock, hp.host, hp.port, serializek((:ok, :done), key)) 76 | break 77 | else 78 | msg = try 79 | res = _exec(mod, val) 80 | serializek((:ok, res), key) 81 | catch exc 82 | # rethrow() 83 | serializek((:error, repr(exc)), key) 84 | end 85 | send(sock, hp.host, hp.port, msg) 86 | end 87 | end 88 | close(sock) 89 | println("EvalServer $mod done") 90 | end 91 | 92 | function _exec(m, val) 93 | # println("received: $val") 94 | exec(m, Val(first(val)), val[2:end]...) 95 | end 96 | function exec(m, ::Val{:eval}, str::String) 97 | Core.eval(m, Meta.parse(str)) 98 | end 99 | function exec(m, ::Val{:eval}, sym::Symbol) 100 | Core.eval(m, sym) 101 | end 102 | function exec(m, ::Val{:call}, sym::Symbol, args) 103 | Base.invokelatest(Core.eval(m, sym), args...) 104 | end 105 | function exec(m, ::Val{:call}, str::String, args) 106 | Core.eval(m, Meta.parse(str))(args...) 107 | end 108 | function exec(m, ::Val{:set}, x::Symbol, arg) 109 | (Core.eval(m, :($x = $arg)); nothing) 110 | end 111 | function exec(m, ::Val{:set}, xs::Vector{Any}, args::Vector{Any}) 112 | for (i, x) in enumerate(xs) 113 | Core.eval(m, :($x = $(args[i]))) 114 | end 115 | nothing 116 | end 117 | function exec(_, cmd, args...) 118 | throw(ArgumentError("cannot $cmd $args")) 119 | end 120 | 121 | isexit(msg::Vector{UInt8}) = String(copy(msg)) == "exit" 122 | isexit(msg) = msg == :exit 123 | 124 | """ 125 | ``` 126 | pServer(port::Integer) 127 | pServer(host::IPAddr, port::Integer, key::AbstractString) 128 | ``` 129 | 130 | Start a server listening to an UDP `port` and starting 131 | parallel `EvalServer`s if requested. 132 | """ 133 | function pServer(host::IPAddr, port::Integer, key::AbstractString) 134 | sock = UDPSocket() 135 | if bind(sock, host, port) 136 | Threads.@spawn _listen(sock, port, key) 137 | else 138 | println("host $host, port $port not available") 139 | close(sock) 140 | end 141 | end 142 | pServer(port::Integer) = pServer(Sockets.localhost, port, "") 143 | 144 | # listen to a UDP socket and start a parallel 145 | # EvalServer if requested. 146 | function _listen(sock, port, key) 147 | while true 148 | hp, msg = recvfrom(sock) 149 | isexit(msg) && break 150 | val = deserializek(msg, key) 151 | if val == :exit 152 | send(sock, hp.host, hp.port, serializek((:ok, :done), key)) 153 | break 154 | elseif val == :srv 155 | md = parServer(hp, key) 156 | # println("parServer $md") 157 | else 158 | println("parServer cannot $val") 159 | end 160 | end 161 | println("pServer at port $port done") 162 | close(sock) 163 | end 164 | 165 | # Start a parallel EvalServer at a random port and 166 | # respond over its socket to the requesting client 167 | function parServer(client::Sockets.InetAddr, key::AbstractString) 168 | newKey = isempty(key) ? "" : genpasswd(24) 169 | md = client.host == Sockets.localhost ? eServer(0) : eServer(getipaddr(), 0, newKey) 170 | isempty(key) ? 171 | send(md._socket, client.host, client.port, serializek((:ok, md), key)) : 172 | send(md._socket, client.host, client.port, serializek((:ok, newKey, md), key)) 173 | md 174 | end 175 | 176 | function recv_erl(socket::UDPSocket, timeout::Real=5) 177 | cond = Condition() 178 | Timer(_ -> notify(cond), timeout) 179 | t = @async begin 180 | msg = recv(socket) 181 | notify(cond) 182 | msg 183 | end 184 | wait(cond) 185 | t.state == :done ? 186 | deserialize(fetch(t)) : 187 | :timeout 188 | end 189 | 190 | function recv_erl(port::Integer, timeout::Real=5) 191 | s = UDPSocket() 192 | rec = if bind(s, ip"127.0.0.1", port) 193 | recv_erl(s, timeout) 194 | else 195 | println(stderr, "port $port is not available") 196 | nothing 197 | end 198 | close(s) 199 | rec 200 | end 201 | 202 | function send_erl(host::IPAddr, port::Integer, msg) 203 | s = UDPSocket() 204 | send_erl(s, host, port, msg) 205 | close(s) 206 | end 207 | 208 | function send_erl(s::UDPSocket, host::IPAddr, port::Integer, msg) 209 | send(s, host, port, serialize(msg)) 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erjulix 2 | 3 | Connecting Erlang, Julia, Elixir 4 | 5 | [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://pbayer.github.io/erjulix/stable) 6 | [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://pbayer.github.io/erjulix/dev) 7 | [![Build Status](https://github.com/pbayer/erjulix/workflows/CI/badge.svg)](https://github.com/pbayer/erjulix/actions) 8 | [![Coverage](https://codecov.io/gh/pbayer/erjulix/branch/master/graph/badge.svg)](https://codecov.io/gh/pbayer/erjulix) 9 | 10 | ## Project 11 | 12 | This is my ambitious little project to connect the different worlds of Erlang/Elixir and Julia: 13 | 14 | - Provide one package for three platforms/languages, 15 | - Allow them to talk to and call each other. 16 | 17 | Now Erlang and Elixir processes can send messages to each other since they run on the same BEAM platform and share PIDs. But how about sending messages to Julia and back to Erlang/Elixir? 18 | 19 | ## A sample session 20 | 21 | In the Julia REPL we start a `pServer` task, which on demand spawns an `EvalServer` task with its own module namespace. 22 | 23 | ```julia 24 | julia> using Erjulix, Sockets 25 | 26 | julia> pServer(6000) 27 | Task (runnable) @0x0000000110b30ab0 28 | ``` 29 | 30 | In the Elixir REPL we request a Julia `EvalServer` and use it to 31 | evaluate Julia expressions or to call Julia functions. 32 | 33 | ```elixir 34 | iex(1)> {:ok, jl, _} = :ejx_udp.srv(6000) # get an eval server from Julia 35 | {:ok, {{127, 0, 0, 1}, 54465}, "Main.##esm#257"} 36 | iex(2)> :ejx_udp.eval(jl, "using .Threads") 37 | {:ok, []} 38 | iex(3)> :ejx_udp.call(jl, :threadid) 39 | {:ok, 3} 40 | iex(4)> :ejx_udp.call(jl, :factorial, [50]) 41 | {:error, 42 | "OverflowError(\"50 is too large to look up in the table; consider using `factorial(big(50))` instead\")"} 43 | iex(5)> :ejx_udp.eval(jl, """ # define a function on the Julia server 44 | ...(5)> function fact(x) 45 | ...(5)> factorial(big(x)) 46 | ...(5)> end 47 | ...(5)> """) 48 | {:ok, "Main.##esm#257.fact"} 49 | iex(6)> :ejx_udp.call(jl, :fact, [50]) 50 | {:ok, 30414093201713378043612608166064768844377641568960512000000000000} 51 | iex(7)> :timer.tc(:ejx_udp, :call, [jl, :fact, [55]]) 52 | {527, 53 | {:ok, 54 | 12696403353658275925965100847566516959580321051449436762275840000000000000}} 55 | ``` 56 | 57 | The last timing shows that the ping-pong for calling the created Julia `fact` function with data from Elixir and getting the result back takes roughly 500 µs with both sessions running on the same machine (MacBook Pro). 58 | 59 | ```elixir 60 | iex(8)> a = Enum.map(1..10, fn _ -> :rand.uniform() end) 61 | [0.9414436609049482, 0.08244595999142224, 0.6727398779368937, 62 | 0.18612089183158875, 0.7414592106015152, 0.7340558985797445, 63 | 0.9511971092470349, 0.7139960750204088, 0.31514816254491884, 0.94168140313657] 64 | iex(9)> :ejx_udp.set(jl, :a, a) # create variable a on the Julia server 65 | {:ok, []} 66 | ``` 67 | 68 | Back in the Julia REPL: 69 | 70 | ```julia 71 | julia> exmod = Erjulix._ESM[1] # get access to the server module 72 | Main.##esm#257 73 | 74 | julia> exmod.a # and to the created variable a 75 | 10-element Vector{Any}: 76 | 0.9414436609049482 77 | 0.08244595999142224 78 | 0.6727398779368937 79 | 0.18612089183158875 80 | ⋮ 81 | 0.9511971092470349 82 | 0.7139960750204088 83 | 0.31514816254491884 84 | 0.94168140313657 85 | 86 | julia> using Plots .... 87 | ``` 88 | 89 | ### Working remotely 90 | 91 | If we start our `pServer` with the machine's IP address and a key, communication with remote clients gets SHA-256 encrypted: 92 | 93 | ```julia 94 | julia> getipaddr() 95 | ip"192.168.2.113" 96 | 97 | julia> key = Erjulix.genpasswd(12) 98 | "1XQeFem2NUNw" 99 | 100 | julia> pServer(getipaddr(), 6000, key) 101 | Task (runnable) @0x00000001110e7b90 102 | ``` 103 | 104 | We use the machine's IP address and that key to access the `pServer` from a Raspberry Pi in the local network: 105 | 106 | ```elixir 107 | iex(1)> :inet.gethostname() 108 | {:ok, 'raspberrypi'} 109 | iex(2)> key = "1XQeFem2NUNw" 110 | "1XQeFem2NUNw" 111 | iex(3)> {:ok, jl, _} = :ejx_udp.srv({{192,168,2,113}, 6000, key}) 112 | {:ok, {{192, 168, 2, 113}, 55052, "j8Gh3G6dPfJm28UpthL0dXew"}, "Main.##esm#258"} 113 | iex(4)> :ejx_udp.call(jl, :factorial, [20]) 114 | {:ok, 2432902008176640000} 115 | iex(5)> :timer.tc(:ejx_udp, :call, [jl, :factorial, [20]]) 116 | {86620, {:ok, 2432902008176640000}} 117 | ``` 118 | 119 | The `pServer` generated a new key for encrypted network access to the Julia `EvalServer`. The timing shows that network ping-pong took under 100 ms between the two machines (without encryption it takes around 70 ms). 120 | 121 | ```elixir 122 | iex(9)> :ejx_udp.client(jl, :exit) 123 | {:ok, :done} 124 | ``` 125 | 126 | ## Rationale 127 | 128 | This is a prototype for interoperability based on [Erlang`s Term Format](http://erlang.org/doc/apps/erts/erl_ext_dist.html) over UDP. 129 | 130 | - It is aimed at experimenting and learning before providing Julia [Actors](https://github.com/JuliaActors/Actors.jl) with functionality for sharing messages with Erlang/Elixir. 131 | - It allows applications in Web services, IoT or microservices. 132 | - A more general application, providing message-based interop also with other languages should be done with [OSC](http://opensoundcontrol.org). 133 | 134 | ## Caveats 135 | 136 | **Thread-safety:** Of course accessing the server module as demonstrated is not thread-safe and thus should not be done concurrently. 137 | 138 | **Security:** If you share UDP-Server addresses and ports, a remote client can get access to the filesystem. If you provide 139 | a key to the `pServer`, data transmissions will use SHA-256 encryption. 140 | 141 | ## ToDo 142 | 143 | - [x] Implement [JWT](https://jwt.io) tokenized secure data transmission, 144 | - [ ] Implement an Elixir server to serve Julia with Elixir/Erlang functionality. 145 | 146 | ## Dependencies 147 | 148 | - The Julia package currently depends on [`ErlangTerm.jl`](https://github.com/helgee/ErlangTerm.jl). 149 | - The Erlang/Elixir part depends on [a fork](https://github.com/pbayer/jwerl) of `jwerl`, compatible with Erlang/OTP 24. There is [an issue](https://gitlab.com/glejeune/jwerl/-/issues/18) to update the main repo. 150 | 151 | ## Installation 152 | 153 | When available in the Julia registry, you can install the package with 154 | 155 | ```julia 156 | pkg> add Erjulix 157 | ``` 158 | 159 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed in Elixir by adding `erjulix` to your list of dependencies in `mix.exs`: 160 | 161 | ```elixir 162 | def deps do 163 | [ 164 | {:erjulix, "~> 0.1.0"} 165 | ] 166 | end 167 | ``` 168 | 169 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 170 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 171 | be found at [https://hexdocs.pm/erjulix](https://hexdocs.pm/erjulix). 172 | --------------------------------------------------------------------------------