Call Golang function from EasyLanguage (TradeStation)

Wouldn’t it be great if you could use Go functions in EasyLanguage?

How

Here’s the flow you should follow.

  1. Create some func in Go
  2. Create a static library with c-archive option
  3. Create a wrapper function with __stdcall in C
  4. Build a dll file
  5. Then use the dll file in EasyLanguage

GitHub

https://github.com/yat1ma30/go-tradestation-starter

Env

Build 32 bit Go

TradeStation is a 32 bit app, so…

$ set GOARCH=386
$ set goroot_bootstrap=%goroot%
$ cd %goroot%\src
$ make.bat --no-clean

Create some func in Go

Whatever you want. You can use any Go libraries. You can also use goroutine.

For example, Here’s the simple function that returns float64.

In main.go,

package main

import "C"

//export CalcDouble
func CalcDouble(x float64) float64 {
	return 2 * x
}

// empty main func is needed
func main() {}

Well, in this step, you have to care about the following.

  • If you want to take some string argument, you should copy the value to let Go GC manage it
    • You must do that in main goroutine

e.g .

func manageStrInGo(s *string) {
	*s = string([]byte(*s))
}
  • Go function which returns string may not work correctly in EL
    • I dunno why :(

Build a static library with c-archive option

TradeStation is a 32 bit application, so you have to use 32 bit Go.

$ set GOARCH=386
$ set CGO_ENABLED=1
$ set CC=gcc
$ go build -o libexample.a -buildmode=c-archive main.go

This command generates:

  • libexample.a
  • libexample.h

Maybe you should NOT use c-shared option to create a dll file directly, because the Calling convention in that dll is __cdecl while EasyLanguage can treat only __stdcall function. So you have to wrap the __cdecl function with __stdcall function (next section).

Create a wrapper function with __stdcall in C

e.g. example.cpp

#include "libexample.h"

#ifdef __cplusplus
extern "C" {
#endif
GoFloat64 __stdcall _CalcDouble(GoFloat64 p0) {
  return CalcDouble(p0);
}
#ifdef __cplusplus
}
#endif

and example.def

LIBRARY example.dll
EXPORTS
CalcDouble = _CalcDouble

You can automate this process with some script.

FYI (WARNING: This Python3 example is too dirty and unstable lel),

import sys
import os
import typing
import re

def extract_func_lines(header: str) -> typing.List[str]:
  """Extracts function lines from *.h file exported by Go
  returns: e.g. ['GoFloat64 EchoDouble(GoFloat64 p0)', ...]
  """
  with open(header, 'r') as f:
    return [
      line.strip().replace('extern ', '').rstrip(';')
      for line in f.readlines()
      if line.startswith("extern ") and '"C"' not in line
    ]

def generate_c_func(func_line: str) -> str:
  """
  returns: e.g.
  from:
  GoFloat64 EchoDouble(GoFloat64 p0)
  to:
  GoFloat64 __stdcall _EchoDouble (GoFloat64 p0) {
    return EchoDouble(p0)
  }
  """
  # retrun type
  return_type = func_line.split()[0]
  gostring_p = ''
  if return_type == "GoString":
    return_type = 'const char*'
    gostring_p = '.p'
  func = " ".join(func_line.split()[1:]).replace('GoString', 'const char*')
  func_name = get_func_name(func_line)
  matches = re.findall(func_name + r'\((.+)\)', func_line)
  args = []
  if len(matches) != 0:
    arg_str_list = matches[0].split(',')
    for arg in arg_str_list:
      arg = arg.strip()
      isGoString = arg.startswith('GoString')
      var = arg.split()[-1]
      args.append(f'newGoString({var})' if isGoString else var)
  args_str = ", ".join(args)
  return f"{return_type} __stdcall _{func} {{\n  return {func_name}({args_str}){gostring_p};\n}}"


C_CODE_TEMPLATE = """#include "{0[header]}"
#include <cstring>

#pragma execution_character_set("utf-8")


inline GoString newGoString(const char* s) {{
  return GoString{{s, static_cast<ptrdiff_t>(strlen(s))}};
}}


#ifdef __cplusplus
extern "C" {{
#endif
{0[functions]}
#ifdef __cplusplus
}}
#endif
"""

def generate_c_code(header: str, func_lines: str) -> str:
  functions =  "\n\n".join([generate_c_func(line) for line in func_lines])
  return C_CODE_TEMPLATE.format({"header": header, "functions": functions})


def get_func_name(func_line: str) -> str:
  return re.sub(
    r'\(.+$', '',
    func_line.split()[1])


DEF_CODE_TEMPLATE = """LIBRARY {0[export_name]}.dll
EXPORTS
{0[def_lines]}
"""

def get_def_line(func_line: str) -> str:
  func_name = get_func_name(func_line)
  return '{0} = _{0}'.format(func_name)

def generate_def_code(export_name: str, func_lines: typing.List[str]) -> str:
  return DEF_CODE_TEMPLATE.format({
    'export_name': export_name,
    'def_lines': '\n'.join(map(get_def_line, func_lines))
  })

def main():
  # read header file
  header: str = sys.argv[1]  # libfoo.h
  # libfoo.h -> foo
  export_name = re.sub(
    r'^lib', '',
    header.split('.')[0])
  if not os.path.exists(header):
    print(f"{header} does not exist")
    sys.exit(1)
  # generate cpp code using the header file
  func_lines = extract_func_lines(header)
  c_code = generate_c_code(header, func_lines)
  def_code = generate_def_code(export_name, func_lines)
  # write
  with open(f"{export_name}.cpp", 'w') as f:
    f.write(c_code)
  with open(f"{export_name}.def", 'w') as f:
    f.write(def_code)

if __name__ == "__main__":
  main()

Create a dll file

Run the following command to create example.dll.

$ gcc -o example.dll example.def -mdll example.cpp -L. -lexample -lwinmm -lws2_32 -lntdll

Use the dll file in EasyLanguage

First, copy the dll file into TradeStation Program folder so that Trade station can see it. I put the dll file like C:\Program Files (x86)\TradeStation 9.5\Program\yat1ma30\example.dll.

Then just call the function from EL and be happy.

using elsystem;

external: "yat1ma30\example.dll", double, "CalcDouble", double;

once begin
  ClearPrintLog;
  Print(CalcDouble(2.5)); // 5
end;

Conclusion

You are free to Go even in TradeStation 🎉🎉🎉

How to host a dedicated Unturned 3.0 server using SteamCMD (VPS)

comments powered by Disqus