Wouldn’t it be great if you could use Go functions in EasyLanguage?
How
Here’s the flow you should follow.
- Create some func in Go
- Create a static library with
c-archive
option - Create a wrapper function with
__stdcall
in C - Build a dll file
- Then use the dll file in EasyLanguage
GitHub
https://github.com/yat1ma30/go-tradestation-starter
Env
- Windows 10 64-bit
- TradeStation 9.5
- gcc 5.1.0 (tdm-1)
- http://tdm-gcc.tdragon.net/download
- append
C:\<your-folder>\TDM-GCC-32\bin\
to path
- go version go1.12.1 windows/amd64
- Cmder (whatever)
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 🎉🎉🎉