Skip to content

Commit

Permalink
- Added the winim package as a dependency;
Browse files Browse the repository at this point in the history
- Added compilation tips and errors;

- Added custom `DllMain()`;

- `NimMain()` is no longer called in `DllMain()`, as it calls WINAPI functions that should not be called in `DllMain()`;

- `GC_fullCollect()` and `NimDestroyGlobals()` are called in `DllMain` when the dll is unloaded;

- Compile with `-d:mdlldkNoDllMain` to add a custom `DllMain()`;

- `NimMain()` is called from `LoadDll()` and from all procedures added with `newProcToExport()`, `newProcToExportW()` and `newProcToExportA()` if not already called;

- The procedures mMajor(), `mMinor()`, `mBeta()`, `mUnicode()`, `mMaxBytes()`, `mKeepLoaded()`, `mMainWindowHandle()`, `mRawVersion()`, `mToCStringAndCopy()` and `mToWideCStringAndCopy()` are now independent of the use of the `addLoadProc()` template;

- Added the `mInitialized()` procedure.

- The `addAliasFor()` compile-time procedure is no longer exportable;

- Added the `addAliasFor()` template;

- Procedures added with `newProcToExport()` now call a procedure with the abstraction layer instead of adding abstraction code in every added procedure. However, the compiler can optimize and do inline.

- All documentation comments at the top of the body of calls to `addLoadProc()`, `addUnloadProc()`, `newProcToExport()`, `newProcToExportW()`, `newProcToExportA()` and `addAliasFor()` are passed to the documentation generated by Nim with `nim doc`.

- The minimum required Nim compiler is now version 2.0.0, due to the annotation with pragma `aliasFor` in templates generated with `nim doc`.
  • Loading branch information
rockcavera committed Mar 30, 2024
1 parent 6561f99 commit 9d50b5b
Show file tree
Hide file tree
Showing 15 changed files with 871 additions and 529 deletions.
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ This package is a Dynamic-link libraries (DLLs) Development Kit for mIRC made in

### But why use the Nim programming language to create dlls for mIRC?

Well, if you don't know the Nim programming language, I invite you to visit the site (https://nim-lang.org/) that has a description and possible reasons for you to venture into it.
Well, if you don't know the Nim programming language, I invite you to visit the site (https://nim-lang.org/) that has a description and possible reasons for you to venture into it.

However, I will give my view here. Nim is an easy language to start and evolve, but there are also advanced features that need more effort. Provides C-like performance. It has several backends (C, C++, Objective C and Javascript) making it possible to work in both backend and frontend development. It offers a bidirectional interface with the target backend, making it possible to call code from the backend to Nim or call the Nim code from the backend. In theory it compiles for any architecture and operating system that supports C (may need minor tweaks). There are several methods for managing memory (garbage collectors), but the preferred one for mIRC dlls is ORC [[1]](https://forum.nim-lang.org/t/8897#58114). Finally, I could not fail to mention the package manager Nimble, which facilitates the installation of new packages that extend the language.

### Why use this package?

The mdlldk package brings templates that add the standard procedures of an mIRC dll, such as: LoadDll() and UnloadDll(); as well as facilitating the export of procedures, as it automatically creates the .def file with the symbols.

When exporting the LoadDll() procedure with the `addLoadProc()` template, procedures will be added that help in the development of your dll. The list of added procedures can be seen [here](https://rockcavera.github.io/nim-mdlldk/mdlldk/onlydocumentation.html#12).
The package adds procedures that help in the development of your dll. All added procedures can be seen [here](https://rockcavera.github.io/nim-mdlldk/mdlldk/tools.html).

The `newProcToExport()` template, which adds a procedure exported to dll and creates an entire abstraction to enable it to work in both unicode and non-unicode mIRC, that is, from version 5.6 (5.60), when support for dlls was added, up to the latest known version of mIRC. If you choose to use `newProcToExport()`, it will not be necessary to manually fill in the `data` or `parms` parameters, as this is done automatically, safely and without exceeding the size allocated by mIRC in the pointers and if it exceeds, it will be truncated to the limit and avoids mIRC crashes. This "magic" is done at runtime and according to each mIRC version, as the memory size allocated to the `data` and `parms` pointers has changed with the mIRC versions.

There are also the `newProcToExportW()` and `newProcToExportA()` templates, which also add an exported procedure to dll, but at a lower level than `newProcToExport()`. In the first template the parameters `data` and `parms` will be of type `WideCString`, while in the second they will be `cstring`. Even if you use one of these two templates you can also take advantage of safe copying for `data` and `parms` using `mToWideCStringAndCopy()` or `mToCStringAndCopy()`. Remembering that these last two procedures are only available if the `addLoadProc()` template is called in your code.
There are also the `newProcToExportW()` and `newProcToExportA()` templates, which also add an exported procedure to dll, but at a lower level than `newProcToExport()`. In the first template the parameters `data` and `parms` will be of type `WideCString`, while in the second they will be `cstring`. Even if you use one of these two templates you can also take advantage of safe copying to `data` and `parms` using `mToWideCStringAndCopy()` or `mToCStringAndCopy()`.

Add an alias to a procedure already declared with the `addAliasFor()` template.

Generate your dll documentation file using `nim doc`. All documentation comments at the top of the body of calls to `addLoadProc()`, `addUnloadProc()`, `newProcToExport()`, `newProcToExportW()`, `newProcToExportA()` and `addAliasFor()` are passed to the documentation generated by Nim.

Finally, the `exportAllProcs()` template facilitates the process of exporting procedures to dll, as it generates the .def file with all the symbols that must be exported and links to the dll during the linking process.

Expand Down Expand Up @@ -44,21 +48,24 @@ This is a basic commented example:
# Import the mdlldk package.
import pkg/mdlldk
# Adds procedure LoadDll() and defines that the dll must not continue loaded
# after use and the communication between the dll and mIRC must be by unicode
# (WideCString).
# Adds procedure LoadDll().
addLoadProc(false, true):
## The dll will be unloaded right after use and the communication between
## mIRC and dll will be by the use of unicode strings.
discard
# Adds procedure UnloadDll() and defines that mIRC can unload the dll when it is
# unused for ten minutes.
# Adds procedure UnloadDll().
addUnloadProc(RAllowUnload):
## The dll is allowed to unloaded.
discard
# Adds the `test` procedure which can be called from mIRC like this:
# `/dll test.dll test`
newProcToExport(test):
result.outData = "echo -a Dll test made in Nim " & NimVersion & " for mIRC"
# Adds the `info` procedure, which can be called from mIRC.
newProcToExport(info):
## When called, it executes a command that prints a message about the dll
## in the active window.
##
## Usage: `/dll test.dll info`
result.outData = "echo -a test.dll made in Nim " & NimVersion & " for mIRC"
result.ret = RCommand
# It must be added to the last line of your Nim code to correctly export all
Expand All @@ -67,15 +74,15 @@ exportAllProcs()
```
The above code should be compiled as follows:

`nim c --app:lib --cpu:i386 --gc:orc -d:useMalloc -d:release test.nim`
`nim c --app:lib --cpu:i386 --mm:orc --noMain -d:noRes -d:useMalloc -d:release test.nim`

To learn more about compiler options, visit https://nim-lang.org/docs/nimc.html.

In case you want to produce a smaller dll, you can add such switches:

`nim c --app:lib --cpu:i386 --gc:orc -d:useMalloc -d:danger -d:strip --opt:size test.nim`
`nim c --app:lib --cpu:i386 --mm:arc --noMain -d:noRes -d:useMalloc -d:danger -d:strip -d:lto --threads:off --opt:size test.nim`

With this last line my generated dll had only 18.5KB against 139KB of the other one, using the Nim 1.6.4 and tdm64-gcc-10.3.0-2 compilers.
With this last line my generated dll had only 11.5KB against 81,5KB of the other one, using the Nim 2.0.2 and clang 18.1.1 (i686-w64-windows-gnu) compilers.

# Documentation
https://rockcavera.github.io/nim-mdlldk/mdlldk.html
120 changes: 54 additions & 66 deletions examples/mircutils.nim
Original file line number Diff line number Diff line change
@@ -1,100 +1,86 @@
# This example exports 4 procedures to the generated dll, which are: `dllInfo`, `getPId`,
# `setTitleBar` and `setIcon`. In addition to creating an alias for the `dllInfo` procedure with the
# name `version`.
## This example exports 4 procedures to the generated dll, which are: `dllInfo`,
## `getPId`, `setTitleBar` and `setIcon`. In addition to creating an alias for
## the `dllInfo` procedure with the name `version`.

# Read the comments to understand and know how to use this dll. Some obvious parts of the code were
# not commented and it is necessary to know the documentation of the mdlldk package.
# Read the comments to understand and know how to use this dll. Some obvious
# parts of the code were not commented and it is necessary to know the
# documentation of the mdlldk package.

# Import the mdlldk package.
import mdlldk

# Declares some common usage types in Windows.
# Use as a reference to declare Windows types:
# - https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/cca27429-5689-4a16-b2b4-9325d93e4ba2
# - https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
type
DWORD = uint32 # or cuint
HANDLE = int # or pointer
HICON = HANDLE
LONG_PTR = int
LPARAM = LONG_PTR
LPCSTR = cstring # or ptr char
LRESULT = LONG_PTR
UINT = uint32 # or cuint
UINT_PTR = uint
WPARAM = UINT_PTR
# WINAPI functions, constants and types imported from the `winim` package
from pkg/winim/inc/windef import DWORD, HANDLE, HICON, LONG_PTR, LPARAM, LPCSTR,
LRESULT, UINT, UINT_PTR, WPARAM
from pkg/winim/winstr import winstrConverterStringToPtrChar # To convert `string` to `LPCSTR` (`ptr char`)
from pkg/winim/inc/winuser import DestroyIcon, SendMessageA, SetWindowTextA,
ICON_SMALL, ICON_BIG, WM_SETICON
from pkg/winim/inc/shellapi import ExtractIconExA
from pkg/winim/inc/winbase import GetCurrentProcessId

# Missing constant in `winim` package
const UINT_MAX = UINT(-1) # https://docs.microsoft.com/en-us/cpp/c-runtime-library/data-type-constants?view=msvc-170

# Define a dll version
const
ICON_SMALL = WPARAM(0) # https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-seticon
ICON_BIG = WPARAM(1) # https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-seticon
UINT_MAX = 0xFFFFFFFF'u32 # https://docs.microsoft.com/en-us/cpp/c-runtime-library/data-type-constants?view=msvc-170
WM_SETICON = 0x0080'u32 # https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-seticon
dllVersionMajor = 1 # The major version that we will assign to the dll.
dllVersionMinor = 0 # The minor version that we will assign to the dll.
strDllVersion = $dllVersionMajor & "." & $dllVersionMinor # The string version.

# Here begins the declaration of some procedures that will be imported from certain Windows dlls.

# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroyicon
proc destroyIcon(hIcon: HICON): BOOL {.stdcall, importc: "DestroyIcon", dynlib: "User32.dll".}

# https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-extracticonexa
proc extractIconExA(lpszFile: LPCSTR, nIconIndex: cint, phiconLarge, phiconSmall: ptr HICON, nIcons: UINT): UINT {.stdcall, importc: "ExtractIconExA", dynlib: "Shell32.dll".}

# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocessid
proc getCurrentProcessId(): DWORD {.stdcall, importc: "GetCurrentProcessId", dynlib: "Kernel32.dll".}

# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessagea
proc sendMessageA(hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall, importc: "SendMessageA", dynlib: "User32.dll".}

# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowtexta
proc setWindowTextA(hWnd: HWND, lpString: LPCSTR): BOOL {.stdcall, importc: "SetWindowTextA", dynlib: "User32.dll".}

# Adds procedure LoadDll() and defines that the dll must not continue loaded after use and the
# communication between the dll and mIRC must be by unicode (WideCString).
# Adds procedure LoadDll().
addLoadProc(false, true):
## The dll must not continue loaded after use and the communication between
## the dll and mIRC must be by unicode.
discard

# Adds procedure UnloadDll() and defines that mIRC can unload the dll when it is unused for ten
# minutes.
# Adds procedure UnloadDll().
addUnloadProc(RAllowUnload):
## The dll is allowed to unloaded.
discard

# Displays the dll information in the active mIRC window.
# Use: /dll mircutils.dll dllInfo
# Adds the `dllInfo` procedure, which can be called from mIRC.
newProcToExport(dllInfo):
## Displays the dll information in the active mIRC window.
##
## Usage: `/dll mircutils.dll dllInfo`
result.outData = "echo -a mircutils.dll v" & strDllVersion & " | echo -a Made with mdlldk on Nim - https://github.com/rockcavera/nim-mdlldk"
result.ret = RCommand

# Returns the mIRC process identifier.
# Use: $dll(mircutils.dll, getPId, $null)
# Adds the `getPId` procedure, which can be called from mIRC.
newProcToExport(getPId):
let pId = getCurrentProcessId()
## Returns the mIRC process identifier.
##
## Usage: `$dll(mircutils.dll, getPId, $null)`
let pId = GetCurrentProcessId()
if pId > 0:
result.outData = $pId
else:
result.outData = "$false"
result.ret = RReturn

# Change the text of the title bar of the mIRC main window.
# Use: $dll(mircutils.dll, setTitleBar, <TEXT|$null>)
# Adds the `setTitleBar` procedure, which can be called from mIRC.
newProcToExport(setTitleBar):
if setWindowTextA(mWnd, LPCSTR(data)) > 0:
## Change the text of the title bar of the mIRC main window.
##
## Usage: `$dll(mircutils.dll, setTitleBar, <TEXT|$null>)`
if SetWindowTextA(mWnd, LPCSTR(data)) > 0:
result.outData = "$true"
else:
result.outData = "$false"
result.ret = RReturn

# Changes the mIRC icon that is displayed in the title bar, alt + tab and Windows toolbar.
# Use: $dll(mircutils.dll, setIcon, <FILE.ICO>)
# Adds the `setIcon` procedure, which can be called from mIRC.
newProcToExport(setIcon):
## Changes the mIRC icon that is displayed in the title bar, alt + tab and
## Windows toolbar.
##
## Usage: `$dll(mircutils.dll, setIcon, <FILE.ICO>)`
var
largeIcon: HICON
smallIcon: HICON
let rExt = extractIconExA(LPCSTR(data), cint(0), cast[ptr HICON](addr largeIcon),
cast[ptr HICON](addr smallIcon), UINT(1))

let rExt = ExtractIconExA(LPCSTR(data), 0, cast[ptr HICON](addr largeIcon),
cast[ptr HICON](addr smallIcon), 1)

if rExt == UINT_MAX or rExt == 0: # If an error occurs, it will return `UINT_MAX`. If no icon is exported, it will return `0`.
result.outData = "$false"
Expand All @@ -104,22 +90,24 @@ newProcToExport(setIcon):
if smallIcon == 0:
smallIcon = largeIcon

smallIcon = cast[HICON](sendMessageA(mWnd, WM_SETICON, ICON_SMALL, cast[LPARAM](smallIcon))) # Send the new icon and return the old one if it exists
largeIcon = cast[HICON](sendMessageA(mWnd, WM_SETICON, ICON_BIG, cast[LPARAM](largeIcon))) # Send the new icon and return the old one if it exists
smallIcon = cast[HICON](SendMessageA(mWnd, WM_SETICON, ICON_SMALL, LPARAM(smallIcon))) # Send the new icon and return the old one if it exists
largeIcon = cast[HICON](SendMessageA(mWnd, WM_SETICON, ICON_BIG, LPARAM(largeIcon))) # Send the new icon and return the old one if it exists

if smallIcon != 0:
discard destroyIcon(smallIcon) # Destroy the old icon
discard DestroyIcon(smallIcon) # Destroy the old icon
if smallIcon != 0:
discard destroyIcon(largeIcon) # Destroy the old icon
discard DestroyIcon(largeIcon) # Destroy the old icon

result.outData = "$true"

result.ret = RReturn

static:
# Adds an alias named `version` to the `dllInfo` procedure.
# Use: /dll mircutils.dll version
addAliasFor("dllInfo", "version")
# Adds an alias named `version` to the `dllInfo` procedure.
addAliasFor(dllInfo, version):
## Is an alias of procedure `dllInfo()`.
##
## Usage: `/dll mircutils.dll version`
discard

# It must be added to the last line of your Nim code to correctly export all symbols to the dll.
exportAllProcs()
4 changes: 2 additions & 2 deletions mdlldk.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.1.0"
version = "0.2.0"
author = "rockcavera"
description = "Dynamic-link libraries (DLLs) Development Kit for mIRC."
license = "MIT"
Expand All @@ -9,4 +9,4 @@ srcDir = "src"

# Dependencies

requires "nim >= 1.4.0"
requires "nim >= 2.0.0", "winim >= 3.9.2"
Loading

0 comments on commit 9d50b5b

Please sign in to comment.