Erlang "init terminating in do_boot" error when calling Elixir from .NET

Hello! I am working on something where I am trying to pass data between .NET, namely F#, and Elixir through the command line. On Windows, I am getting a strange error that I can’t figure out.

Here is my F# function:

let executeProcess name arguments =
    let startInfo = System.Diagnostics.ProcessStartInfo(
        FileName = name,
        Arguments = arguments,
        UseShellExecute = false,
        RedirectStandardError = true,
        RedirectStandardOutput = true
    )
    use p = new System.Diagnostics.Process(StartInfo = startInfo)
    p.Start() |> printfn "Successfull?: %A"
    p.WaitForExit()
    p.StandardOutput.ReadToEnd()

On Ubuntu, this works without issue (you can try by copy and pasting this into a dotnet fsi session after downloading the latest .NET SDK or you can try it in a Polyglot Notebook in VS Code, which requires no install I believe as it installs F# for you):

$ dotnet fsi

Microsoft (R) F# Interactive version 12.4.0.0 for F# 7.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> <copy and paste above function>;;
val executeProcess: name: string -> arguments: string -> string

> executeProcess "dotnet" "--version";;
Successfull?: true
val it: string = "7.0.101
"

> executeProcess "elixir" "--version";;
Successfull?: true
val it: string =
  "Erlang/OTP 25 [erts-13.1.4] [source] [64-bit] [smp:20:20] [ds:20:20:10] [async-threads:1] [jit:ns]

Elixir 1.14.3 (compiled with Erlang/OTP 25)
"

>

However, on Windows, I get a strange Erlang init error but notice that Elixir works just fine on the command line:

PS > elixir.bat --version
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:20:20] [ds:20:20:10] [async-threads:1] [jit:ns]

Elixir 1.14.2 (compiled with Erlang/OTP 25)
PS > dotnet fsi

Microsoft (R) F# Interactive version 12.8.0.0 for F# 8.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> <copy and paste above command>;;
val executeProcess: name: string -> arguments: string -> string

> executeProcess "dotnet" "--version";;
Successfull?: true
val it: string = "8.0.100-preview.6.23330.14
"

> executeProcess "elixir.bat" "--version";;
Successfull?: true
val it: string =
  "{"init terminating in do_boot",{undef,[{elixir,start_cli,[],[]},{init,start_em,1,[{file,"init.erl"},{line,1220}]},{init,do_boot,3,[{file,"init.erl"},{line,910}]}]}}
"

This is where I get this weird error of {"init terminating in do_boot",{undef,[{elixir,start_cli,[],[]},{init,start_em,1,[{file,"init.erl"},{line,1220}]},{init,do_boot,3,[{file,"init.erl"},{line,910}]}]}}. Searching and even looking up the line numbers in init.erl, this error seems to be because of Erlang version issues. However, that is not an issue on my machine. Elixir and Erlang via iex and erl work just fine, as does elixir.bat --version from the command line.

There seems to be something in how the .NET process launcher is interacting with the Elixir.bat that one or the other doesn’t like. Does anyone know what’s going on here? Is this an issue with elixir.bat on Windows?

(Note: There is a workaround, and it is to actually make the filename "powershell" with arguments "elixir --version". However, I don’t know why that works and the other doesn’t. The Elixir binaries/scripts are in my path, as are the Erlang binaries.)


Edit: I went ahead and tried this on the latest versions of Elixir 1.15.4 and Erlang/OTP 26. The Erlang "init terminated in do_boot" error seems to have gone away, but nothing is written to standard out when running this code as above. However, if I remove the redirection from standard out and reading, I get the following error in FSI:

> let executeProcess name arguments =
-     let startInfo = System.Diagnostics.ProcessStartInfo(
-         FileName = name,
-         Arguments = arguments
-     )
-     use p = new System.Diagnostics.Process(StartInfo = startInfo)
-     p.Start() |> printfn "Successfull?: %A"
- ;;
val executeProcess: name: string -> arguments: string -> unit

> executeProcess "elixir.bat" "--version";;
Successfull?: true
val it: unit = ()

> Error! Failed to load module 'elixir' because it cannot be found. Make sure that the module name is correct and
that its .beam file is in the code path.

Runtime terminating during boot ({undef,[{init,start_it,1,[{_},{_}]},{init,start_em,1,[{_},{_}]},{init,do_boot,3,[{_},{_}]}]})

Crash dump is being written to: erl_crash.dump...done

Did you install elixir from something that requires some environment from your shell, like asdf for instance?

The issue appears on Windows, where I installed Erlang and Elixir via the official Windows installers, and asdf is not supported on Windows. I use asdf on Ubuntu, but this works fine there.

This has to somehow be related to how on Windows, Elixir is wrapped via scripts, and one isn’t calling binaries directly. But it’s strange how it works fine on the command line. I’m not sure there’s much else I would need to or can do on the .NET side.

I believe you’re right about this. You could call erl.exe with the args that the batch script passes to it.

I presume you don’t want to flip things and have elixir call F# via a port, because that’s not too hard. There is an ETF library in F#.

I presume you don’t want to flip things and have elixir call F# via a port, because that’s not too hard. There is an ETF library in F#.

Haha. :slight_smile: The context of my question is actually to help test and verify an encoding/decoding library I wrote myself in F# for the Erlang External Term Format. My main goal is playing around with some ideas around an Elixir Port and F# interaction, and so I wrote my own ETF library in F# since it’s not too hard and gives me control when integrating it into the port abstraction. This question is to help test the F# encoding/decoding across the various OS platforms using both regular tests and property-based tests. The example function I gave is similar to the real one, which is used in tests to write out a string to Elixir, have Elixir convert it to an Erlang term, then F# decodes it and tests it against what’s expected. Something like this:

[<Fact>]
let ``Decode tuple from stream`` () =
    @"{:ok, :another}" |> writeAndDecode |> should equal (ErlangTerm.Tuple [ErlangTerm.Atom "ok"; ErlangTerm.Atom "another"])

Calling elixir.bat --version as an argument to the powershell command works, but it isn’t entirely clear to me why that works but calling the script directly doesn’t work. Mainly trying to know why to further my understanding of how both Elixir and F# interact with the command line. It also seems like an issue with the Erlang crash dump when simply calling elixir.bat --version.

Very well. You do have to think about passing env vars on to child/sub processes, something I discovered when playing with porting elixir.bat to a Zig executable.

ETF library has caused me no issues other than the poor choice of namespaces.

You do have to think about passing env vars on to child/sub processes

It’s possible that that’s what PowerShell is doing properly when I use powershell elixir.bat --version. I have no idea though.

ETF library has caused me no issues other than the poor choice of namespaces.

Which ETF F# library are you using? Last time I checked there were a couple, including one for the BERT term format. My library just does a subset of the base ETF format, since some of them don’t make sense in the context I am thinking of. The encoding/decoding is rather easy, so I’ll probably keep my own, but it would be nice to see other implementations, especially if they’ve been kept up to date.

After setting things up, I loop over this:

// reply ok on startup
    sendOk (encode "0")
    try
        while true do
            inputBufferSize <- stdin.Read(inputBuffer, 0, 4)
            // input buffer should be size 4, which tells us the size of the message
            match inputBufferSize with
            | 4 -> ()
            | 0 ->
                logger.Debug "Port shutdown, exiting"
                cleanup logger connection
                Environment.Exit(int ReturnCode.Ok)
            | size ->
                logger.Error $"Expected 4 bytes, got {size} containing '{Encoding.UTF8.GetString(inputBuffer)}'"
                cleanup logger connection
                Environment.Exit(int ReturnCode.BadMessageSize)
                
            let msgSizeBytes =
                if BitConverter.IsLittleEndian
                then inputBuffer |> Array.take 4 |> Array.rev
                else inputBuffer |> Array.take 4
            let msgSize = BitConverter.ToInt32(msgSizeBytes, 0)
            
            // read the ETF
            inputBufferSize <- stdin.Read(inputBuffer, 0, msgSize)
            
            // decode the ETF
            let term = ETF.Decoder.decodeTerm inputBuffer
            
            match term with
            | Term.Tuple [Term.Binary requestId; msg] ->
                match decodeMessage msg with
                | SomeMessage(arg1, arg2) -> 
                ...