MetaTrader + C++: DLL

Source and maintenance note (September 2026): The 2019 export for post 1972 contains front matter and an empty body. There is no historical tutorial to preserve. This article is a disclosed, title-based reconstruction using current primary MetaQuotes and Microsoft documentation. It demonstrates an offline arithmetic boundary for MetaTrader 5/MQL5; it does not place trades, bypass DLL permissions, contact a broker, or promise financial results.

A DLL runs native code inside the terminal process. A bad signature, stale dependency, out-of-bounds write, blocking call, or untrusted binary can stop or compromise the terminal. Treat the DLL as a small, versioned systems interface—not as a shortcut around MQL or platform safeguards.

When not to use a DLL

Prefer MQL5 or an EX5 library when the work is expressible there. Avoid a native DLL when:

  • the only purpose is ordinary arithmetic, file handling, or data structures already supported by MQL5;
  • remote or MQL5 Cloud Strategy Tester agents are required, because they do not permit DLL calls;
  • the library’s source, publisher, dependencies, or build record cannot be verified;
  • the proposed interface needs C++ objects, STL containers, raw ownership transfer, callbacks, or long-lived MQL memory pointers;
  • credentials, account secrets, API tokens, or private keys would have to be embedded in the binary;
  • failure or latency could block the calling MQL program’s thread.

Use a DLL only for a narrow native dependency or a measured bottleneck. Keep trading decisions, permissions, symbol state, and order operations outside this example.

Trust and visible consent come before loading

MetaTrader 5 labels DLL imports as potentially dangerous. The platform-level Allow DLL imports option supplies a default; an application’s Dependencies tab displays external modules and controls permission for that application. Leave the platform default off when it is not needed, inspect the displayed dependency, and grant permission only for a DLL whose publisher and exact file hash you have verified.

MQL5 uses early binding: a declared DLL can be loaded before OnStart() or OnInit() runs. Therefore, an in-program permission check is useful for logging but cannot replace the platform’s visible consent dialog. Do not hide that dialog, change terminal configuration automatically, or tell users to enable DLLs globally for unknown programs.

The example script uses #property script_show_inputs so its properties window is shown. Keep AutoTrading disabled: this demonstration contains no trading calls and needs no trading permission.

Define one small, versioned ABI

The first interface should use fixed-width integers, double, caller-owned arrays, explicit lengths, caller-owned output storage, and integer status codes. Do not return pointers or C++ objects.

Boundary concern Contract used here
Calling convention __stdcall, as required by MetaQuotes for native imports.
Linkage/export extern "C" plus __declspec(dllexport); verify the final names with DUMPBIN /EXPORTS.
Architecture This walkthrough builds an x64 DLL for an x64 MetaTrader 5 terminal. A 32-bit target is a separate artifact and must be independently inspected and tested.
Versioning MtApiVersion() returns 1; incompatible changes require a new DLL filename and API version.
Memory MQL owns every input and output buffer. The DLL neither retains nor frees them.
Errors 0 means success; other stable integer values identify argument or numeric failures.

Native implementation

Save as mt_safe_math.cpp:

#include <cmath>
#include <cstdint>

#if defined(_WIN32)
#define MT_API extern "C" __declspec(dllexport)
#define MT_CALL __stdcall
#else
#define MT_API extern "C"
#define MT_CALL
#endif

namespace {
constexpr std::int32_t kApiVersion = 1;
constexpr std::int32_t kMaxValues = 1'000'000;

enum Status : std::int32_t {
    kOk = 0,
    kInvalidArgument = 1,
    kInvalidOutput = 2,
    kNonFiniteValue = 3,
};
}

static_assert(sizeof(std::int32_t) == 4);
static_assert(sizeof(double) == 8);

MT_API std::int32_t MT_CALL MtApiVersion() noexcept {
    return kApiVersion;
}

MT_API std::int32_t MT_CALL MtMean(
    const double* values,
    std::int32_t count,
    double* out_mean) noexcept {
    if (out_mean == nullptr) {
        return kInvalidOutput;
    }
    *out_mean = 0.0;

    if (values == nullptr || count <= 0 || count > kMaxValues) {
        return kInvalidArgument;
    }

    double sum = 0.0;
    for (std::int32_t index = 0; index < count; ++index) {
        if (!std::isfinite(values[index])) {
            return kNonFiniteValue;
        }
        sum += values[index];
        if (!std::isfinite(sum)) {
            return kNonFiniteValue;
        }
    }

    *out_mean = sum / static_cast<double>(count);
    return kOk;
}

The non-Windows macro branch exists only so the arithmetic and validation logic can be unit-tested on another development host. MetaTrader still requires the Windows DLL build. There is no custom DllMain; Microsoft recommends keeping it minimal, and MSVC’s /LD can supply a default entry point.

Portable core test

Save as mt_safe_math_test.cpp:

#include <cassert>
#include <cstdint>
#include <limits>

#if defined(_WIN32)
#define MT_CALL __stdcall
#else
#define MT_CALL
#endif

extern "C" std::int32_t MT_CALL MtApiVersion() noexcept;
extern "C" std::int32_t MT_CALL MtMean(
    const double* values,
    std::int32_t count,
    double* out_mean) noexcept;

int main() {
    assert(MtApiVersion() == 1);

    double values[] = {1.0, 2.0, 3.0, 4.0};
    double mean = -1.0;
    assert(MtMean(values, 4, &mean) == 0);
    assert(mean == 2.5);

    mean = -1.0;
    assert(MtMean(nullptr, 4, &mean) == 1);
    assert(mean == 0.0);

    double invalid[] = {1.0, std::numeric_limits<double>::infinity()};
    assert(MtMean(invalid, 2, &mean) == 3);
    assert(MtMean(values, 4, nullptr) == 2);
}

On a host with GCC, the portable core test is:

g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror \
  mt_safe_math.cpp mt_safe_math_test.cpp -o mt_safe_math_test
./mt_safe_math_test

Passing this test does not validate the Windows ABI, export table, MetaTrader permission flow, or MQL declaration. Those are separate gates below.

MQL5 import and smoke script

Save as MQL5/Scripts/MtSafeMathSmoke.mq5:

#property script_show_inputs

#import "mt_safe_math_v1.dll"
int MtApiVersion();
int MtMean(double &values[], int count, double &out_mean);
#import

enum NativeStatus
  {
   MT_OK               = 0,
   MT_INVALID_ARGUMENT = 1,
   MT_INVALID_OUTPUT   = 2,
   MT_NON_FINITE_VALUE = 3
  };

void OnStart()
  {
   bool terminal_dlls=(bool)TerminalInfoInteger(TERMINAL_DLLS_ALLOWED);
   bool program_dlls=(bool)MQLInfoInteger(MQL_DLLS_ALLOWED);
   bool terminal_x64=(bool)TerminalInfoInteger(TERMINAL_X64);

   PrintFormat("dll-default=%s dll-program=%s x64=%s",
               terminal_dlls ? "true" : "false",
               program_dlls ? "true" : "false",
               terminal_x64 ? "true" : "false");

   if(!program_dlls || !terminal_x64)
     {
      Print("Stop: this smoke test requires explicit DLL consent and x64.");
      return;
     }

   int api_version=MtApiVersion();
   if(api_version!=1)
     {
      PrintFormat("Stop: incompatible native API version %d",api_version);
      return;
     }

   double values[]={1.0,2.0,3.0,4.0};
   double mean=0.0;
   ResetLastError();
   int status=MtMean(values,ArraySize(values),mean);
   int mql_error=GetLastError();

   PrintFormat("native-status=%d mean=%.8f mql-error=%d",
               status,mean,mql_error);
   if(status!=MT_OK)
      Print("Native calculation rejected the input; no other action was taken.");
  }

The MQL prototype must match the native parameter order and sizes exactly. The array is passed by reference and its length is passed separately. The DLL treats it as read-only even though the MQL import syntax does not express that native const qualifier.

Build, inspect, and identify the Windows artifact

Use an x64 Native Tools Command Prompt for Visual Studio and a clean build directory. Record the source commit, cl /Bv output, Windows SDK version, complete command, and resulting hash. A repeatable release process is more important than an unrecorded IDE click sequence.

cl /Bv
cl /nologo /std:c++17 /O2 /W4 /WX /EHsc /MT /LD mt_safe_math.cpp ^
  /link /OUT:mt_safe_math_v1.dll /INCREMENTAL:NO
dumpbin /headers mt_safe_math_v1.dll | findstr /i machine
dumpbin /exports mt_safe_math_v1.dll

Stop unless the header reports the intended x64 machine and the export table contains exactly the expected callable names MtApiVersion and MtMean. extern "C" controls C++ name mangling, but decoration rules and calling conventions differ by architecture. Never rename a decorated export until the MQL call “seems to work”; fix and verify the ABI deliberately.

Build twice in fresh directories with the same recorded toolchain and inputs, then compare SHA-256 values. If they differ, investigate the build inputs before calling the process deterministic. Do not claim that the same hash is guaranteed across compiler or SDK upgrades.

Get-FileHash -LiteralPath .\mt_safe_math_v1.dll -Algorithm SHA256
Get-AuthenticodeSignature -LiteralPath .\mt_safe_math_v1.dll |
  Format-List Status,StatusMessage,SignerCertificate

A hash identifies exact bytes but does not establish who produced them. Publish the hash through an authenticated release channel and, for distributed binaries, sign the DLL and verify the expected publisher. Keep the release manifest, signature result, exports, architecture, API version, and test result together.

Strings, arrays, structures, and ownership

MetaQuotes documents important limits that should shape the interface:

MQL value Native-boundary rule
Simple scalars Passed by value unless explicitly declared by reference. Match exact sizes.
double &array[] The DLL receives the start of the data buffer. It does not know ArraySetAsSeries; pass and validate a separate element count.
string by value The DLL receives a pointer to a copied string buffer. Do not retain it.
string & Refers to the original string buffer. Mutation and capacity rules are easy to get wrong; avoid it in a first ABI.
Text protocol Prefer a caller-owned uchar[] produced with an explicit code page such as CP_UTF8, plus a byte length and output capacity. Define whether the terminator is included.
Simple structure Only POD-like structures without strings, classes, pointers, or dynamic arrays are candidates. Mirror packing and field widths explicitly. MQL5 structures are packed by default.
Complex structure or string array Do not pass it to an imported DLL. MetaQuotes explicitly restricts these types.

Never keep an MQL array or string pointer after the imported call returns. Never allocate with new/malloc in the DLL and ask MQL or another runtime to free it. Microsoft documents heap corruption risks when memory or CRT objects cross DLL boundaries with different runtimes. A caller-allocated buffer plus explicit capacity, written only within bounds, is easier to audit.

Error handling, logging, and secrets

Use two error channels without conflating them:

  • the native function returns a documented status code and initializes caller-owned outputs to a safe value on failure;
  • MQL records GetLastError() separately for terminal/runtime diagnostics.

Do not allow C++ exceptions to cross the C ABI. Keep exported functions noexcept; translate internal failures to stable status codes. Do not log entire market datasets, account identifiers, paths containing usernames, or credentials. A useful diagnostic record contains the terminal build and architecture, DLL API version, expected release ID/hash, function name, element count, native status, MQL error, and elapsed time.

The DLL must not contain broker credentials, API keys, signing keys, account passwords, or “hidden” endpoints. Native binaries can be inspected. If a later design needs privileged external access, define a separate threat model and secret store; do not smuggle secrets through this calculation boundary.

Strategy Tester and demo test plan

MetaQuotes states that remote testing agents and MQL5 Cloud agents cannot execute DLL calls. A local agent can call a DLL only when Allow import DLL is enabled. Plan for that constraint; do not try to evade it or silently fall back to unreviewed code.

Use this sequence:

  1. Run the portable core test with invalid, boundary, non-finite, and maximum-size cases.
  2. Build the x64 DLL in a clean environment; inspect headers, dependencies, and exports; verify hash/signature.
  3. Copy the verified, versioned DLL to the terminal data directory’s MQL5/Libraries folder while the terminal is closed.
  4. Open a disposable or demo terminal profile with AutoTrading off and the smallest possible permissions.
  5. Compile the smoke script without warnings, review the Dependencies tab, and explicitly allow only this known DLL.
  6. Run it twice and compare Journal output. Expected result: API 1, status 0, mean 2.50000000, and no trade action.
  7. If Strategy Tester coverage is needed, use a local agent with explicit DLL permission. Mark remote/cloud optimization unsupported.
  8. Exercise removal and rollback before any broader deployment.

Fuzz or stress native functions in a standalone test process rather than risking repeated terminal crashes. Keep exported calls short because MetaQuotes says DLL code executes in the calling module’s thread.

Allowlist, deployment, rollback, and removal

Maintain an allowlist entry with:

  • unique DLL filename and native API version;
  • x64 architecture and expected export names;
  • SHA-256 and expected signer identity/status;
  • source revision, MSVC/SDK versions, and build command;
  • direct dependencies and license;
  • tests passed and approval date.

Deploy the DLL and matching MQL source/EX5 as one reviewed pair. Use versioned filenames so a rollback does not require overwriting a loaded module. Before replacing or removing a DLL: disable the application’s DLL permission, remove the script/EA from the chart, close the terminal, verify it has exited, then restore the previously approved pair or remove the versioned files. Reopen in the demo profile and rerun the smoke test.

Never overwrite a DLL while the terminal may still have it loaded. Do not add the terminal directory to broad search paths, copy dependencies into Windows system directories, or “fix” loading by disabling security controls.

Troubleshooting matrix

Symptom Read-only checks Safe response
Program stops before OnStart() Journal, Dependencies, DLL permission, exact filename Restore permission only after trust verification; confirm file is in MQL5/Libraries.
“Function not found” dumpbin /exports, spelling, API version Rebuild the matching pair; do not guess decorated names.
DLL will not load dumpbin /headers, dumpbin /dependents, terminal x64 status Supply the correct architecture and reviewed dependencies. Do not copy random DLLs from the internet.
Terminal crash or corrupted output Prototype order/types, array length, output pointer, native unit tests Disable imports, close the terminal, remove the candidate DLL, restore the previous approved version.
Local test works; remote optimization fails Tester agent type and official DLL restrictions Mark remote/cloud unsupported or remove the DLL dependency. Do not bypass the restriction.
Hash or signature differs Release manifest, signer, source/toolchain record Quarantine the file and stop deployment until provenance is resolved.

Acceptance and stop criteria

Accept a release only when:

  • source and build inputs are versioned and the clean build procedure is recorded;
  • the exact architecture, dependencies, two export names, API version, SHA-256, and signer state match the allowlist;
  • native positive and negative tests pass, with no sanitizer/static-analysis finding left unexplained;
  • the MQL smoke script compiles cleanly and produces the same expected log twice in a demo profile;
  • AutoTrading remains off and no network, file, credential, or order operation occurs;
  • local Strategy Tester behavior and remote/cloud non-support are documented;
  • rollback and full removal are successfully rehearsed.

Stop immediately on an unexpected dependency/export, permission prompt mismatch, crash, hang, out-of-bounds report, non-deterministic unexplained build, hash/signature mismatch, secret in source/binary/logs, or any attempt to enable live trading or bypass terminal safeguards.

Primary documentation

Leave a Reply