Writing .NET Plugins for TCC
A guide for .NET developers who want to extend TCC (Take Command Console) with custom commands, variables, and functions using C# or other .NET languages.
Overview
TCC supports plugins written as .NET class libraries. A C++/CLI bridge DLL (`TC-DotNetPluginHost64.dll`) loads your .NET assembly at runtime via reflection. Your plugin class implements the `ITCCPlugin` interface and exposes methods that TCC invokes by name.
How It Works
1. TCC inspects the DLL's PE headers. If a CLR metadata directory is present, TCC treats the DLL as a .NET assembly.
2. TCC loads the bridge DLL (once) and calls `TCCDotNetPlugin_Load` to load your assembly.
3. The bridge finds your plugin type (by `ITCCPlugin` interface or by convention), creates an instance, and caches all public methods.
4. TCC calls `Initialize`, `GetPluginInfo`, and later `Shutdown` through the bridge.
5. When a user invokes a command, variable, or function that matches your `Functions` list, TCC calls the corresponding method on your plugin instance via the bridge.
Quick Start
1. Create a Class Library Project
Create a **.NET Framework 4.8** (or **.NET Standard 2.0**) class library project in Visual Studio.
2. Add the SDK
Either:
- Include `DotNetPluginSDK.cs` directly in your project, *or*
- Reference the compiled `TakeCommand.Plugin.SDK.dll`
The SDK provides two types in the `TakeCommand.Plugin` namespace:
| Type | Purpose
|-----------------|-------------------------------------------
| `TCCPluginInfo` | Data class holding plugin metadata
| `ITCCPlugin` | Interface your plugin class must implement
3. Implement `ITCCPlugin`
C#
using System;
using System.Text;
using TakeCommand.Plugin;
public class MyPlugin : ITCCPlugin
{
private static readonly TCCPluginInfo _info = new TCCPluginInfo
{
Name = "MyPlugin",
Author = "Your Name",
Email = "[email protected]",
WWW = "https://example.com",
Description = "A sample TCC plugin written in C#",
Functions = "HELLO,_MYPLUGINVER,@REVERSE",
Major = 1,
Minor = 0,
Build = 1
};
public TCCPluginInfo GetPluginInfo() => _info;
public bool Initialize() => true;
public bool Shutdown(bool end) => true;
// Command: HELLO
public int HELLO(StringBuilder args)
{
Console.WriteLine("Hello from .NET plugin!");
return 0;
}
// Internal variable: %_MYPLUGINVER
public int _MYPLUGINVER(StringBuilder args)
{
args.Clear();
args.Append("1.0.1");
return 0;
}
// Variable function: @REVERSE[text]
public int f_REVERSE(StringBuilder args)
{
char[] chars = args.ToString().ToCharArray();
Array.Reverse(chars);
args.Clear();
args.Append(chars);
return 0;
}
}
4. Build and Deploy
Build your project and copy the resulting `.dll` file into TCC's `PlugIns` directory (typically a subdirectory of the TCC installation directory). TCC loads all DLLs from this directory on startup, or you can load it manually:
PLUGIN /L C:\Path\To\MyPlugin.dll
The `ITCCPlugin` Interface
C#
public interface ITCCPlugin
{
TCCPluginInfo GetPluginInfo();
bool Initialize();
bool Shutdown(bool endProcess);
}
`GetPluginInfo()`
Called once after loading. Return a `TCCPluginInfo` object describing your plugin and the features it provides.
`Initialize()`
Called after the assembly is loaded and the plugin type is instantiated. Perform any one-time setup here. Return `true` on success, `false` on failure (TCC will unload the plugin).
`Shutdown(bool endProcess)`
Called when the plugin is being unloaded. The `endProcess` parameter is `true` when the entire command processor is shutting down, `false` when only your plugin is being unloaded. Return `true` on success.
The `TCCPluginInfo` Class
C#
public class TCCPluginInfo
{
public string Name { get; set; } // Short name (shown in PLUGIN listings)
public string Author { get; set; } // Author's name
public string Email { get; set; } // Author's email
public string WWW { get; set; } // Author's website
public string Description { get; set; } // Brief description
public string Functions { get; set; } // Comma-delimited feature list
public int Major { get; set; } // Major version number
public int Minor { get; set; } // Minor version number
public int Build { get; set; } // Build number
}
The `Functions` String
The `Functions` property is a **comma-delimited** list of the commands, variables, and functions your plugin provides. The prefix on each name determines its type:
| Prefix | Type | TCC Syntax | Method Signature |
|----------|--------------------|------------------|-------------------------------------------|
| *(none)* | Internal command | `HELLO args` | `public int HELLO(StringBuilder args)` |
| `_` | Internal variable | `%_MYVAR` | `public int _MYVAR(StringBuilder args)` |
| `@` | Variable function | `@MYFUNC[args]` | `public int f_MYFUNC(StringBuilder args)` |
| `*` | Keystroke handler | *(not supported) | |
Example:
C#
Functions = "GREET,CALC,_VERSION,_AUTHOR,@UPPER,@LOWER"
This registers two commands (`GREET`, `CALC`), two variables (`_VERSION`, `_AUTHOR`), and two functions (`@UPPER`, `@LOWER`).
> **Important:** Variable function names in the `Functions` list use the `@` prefix (e.g., `@REVERSE`), but the corresponding **method name** uses the `f_` prefix (e.g., `f_REVERSE`). This convention allows a command and a variable function to share the same base name.
Method Signatures
All plugin feature methods must be `public` and follow one of these signatures:
Primary (read-write) â recommended
C#
public int MethodName(StringBuilder args)
The `StringBuilder` contains the arguments passed by TCC. For variables and functions, write your result back into it. The maximum result length is 32,767 characters.
Read-only variant (commands only)
C#
public int MethodName(string args)
Use this when you only need to read the arguments and don't need to modify them.
No-argument variant
C#
public int MethodName()
For commands or variables that take no input.
### What `args` Contains
| Feature Type | Contents of `args` |
|-------------------|------------------------------------------------|
| Internal command | The command line minus the command name |
| Internal variable | Empty (output only â write result into `args`) |
| Variable function | The argument(s) passed inside `@FUNC[...]` |
Return Values:
| Return Value | Meaning |
|--------------------------------------|------------------------------------------------------------------------------|
| `0` | Success |
| `> 0` | Failure â TCC interprets the value as a system error code and displays an error message |
| `< 0` | Failure â your plugin already displayed an error message |
| `0xFEDCBA98` (unchecked) | **Not handled** â TCC continues searching for a matching internal or external command |
The special return value `0xFEDCBA98` is useful when your plugin wants to inspect (and optionally modify) the command line but then pass it on to TCC's built-in handler:
C#
public int MYCOMMAND(StringBuilder args)
{
// Optionally modify args here...
// Then tell TCC we didn't handle it:
return unchecked((int)0xFEDCBA98);
}
Plugin Discovery
TCC uses two strategies to find your plugin type (in order):
1. **Interface match** â Finds the first public, non-abstract class that implements an interface named `ITCCPlugin`.
2. **Convention match** â Finds the first public, non-abstract class that has both a `GetPluginInfo` method and an `Initialize` method.
Strategy 1 is recommended. Strategy 2 exists for cases where you cannot reference the SDK assembly directly.
Complete Example
Below is a complete, working plugin that adds a `GREET` command, a `_GREETCOUNT` variable, and an `@UPPER` function.
C#
using System;
using System.Text;
using TakeCommand.Plugin;
namespace SampleTCCPlugin
{
public class GreetPlugin : ITCCPlugin
{
private int _greetCount;
private readonly TCCPluginInfo _info = new TCCPluginInfo
{
Name = "Greet",
Author = "Jane Developer",
Email = "[email protected]",
WWW = "https://example.com/greet",
Description = "Greeting plugin with variable and function support",
Functions = "GREET,_GREETCOUNT,@UPPER",
Major = 1,
Minor = 2,
Build = 0
};
public TCCPluginInfo GetPluginInfo() => _info;
public bool Initialize()
{
_greetCount = 0;
return true;
}
public bool Shutdown(bool endProcess)
{
// Nothing to clean up
return true;
}
/// <summary>
/// Command: GREET [name]
/// Usage: GREET World
/// Output: Hello, World!
/// </summary>
public int GREET(StringBuilder args)
{
string name = args.ToString().Trim();
if (string.IsNullOrEmpty(name))
name = "World";
Console.WriteLine($"Hello, {name}!");
_greetCount++;
return 0;
}
/// <summary>
/// Internal variable: %_GREETCOUNT
/// Returns the number of times GREET has been called.
/// </summary>
public int _GREETCOUNT(StringBuilder args)
{
args.Clear();
args.Append(_greetCount.ToString());
return 0;
}
/// <summary>
/// Variable function: @UPPER[text]
/// Returns the uppercased version of the input text.
/// </summary>
public int f_UPPER(StringBuilder args)
{
string input = args.ToString();
args.Clear();
args.Append(input.ToUpperInvariant());
return 0;
}
}
}
Using the Plugin in TCC
C:\> plugin /l C:\PlugIns\SampleTCCPlugin.dll
C:\> greet Developer
Hello, Developer!
C:\> echo %_GREETCOUNT
1
C:\> echo @UPPER[hello world]
HELLO WORLD
C:\> plugin /i Greet
| Module: | C:\PlugIns\SampleTCCPlugin.dll |
| Name: | Greet |
| Author: | Jane Developer |
| Email: | [email protected] |
| Web: | https://example.com/greet |
| Description: | Greeting plugin with variable and function support |
| Implements: | GREET,_GREETCOUNT,@UPPER |
| Version: | 1.2 Build 0 |
Targeting a Specific Plugin
When multiple plugins are loaded, a user can target a specific plugin by prefixing the command, variable, or function name with the plugin name and a `$` delimiter:
C:\> Greet$GREET Developer
C:\> echo %Greet$_GREETCOUNT
C:\> echo @Greet$UPPER[text]
This is useful when two plugins register the same feature name.
Limitations
Feature |
Native Plugin |
.NET Plugin |
Internal commands |
Yes |
Yes |
Internal variables |
Yes |
Yes |
Variable functions |
Yes |
Yes |
Keystroke handlers |
Yes |
Not supported* |
Tab completion |
Yes |
Not supported |
Help/Usage callbacks |
Yes |
Not supported |
* Keystroke handlers require passing the native `KEYINFO` struct (containing raw pointers) across the bridge, which is not supported. You may list `*` entries in your `Functions` string, but they will never be invoked.
Project Configuration
Target Framework
- **.NET Framework 4.8** or **.NET Standard 2.0** class library
- The bridge DLL uses `Assembly.LoadFrom()`, so standard .NET Framework class libraries work best
Build Output
- Build as **Class Library** (DLL)
- Ensure the output is an **AnyCPU** or platform-matching assembly (x64 for 64-bit TCC, x86 for 32-bit TCC)
- The DLL **must** contain CLR metadata (this is automatic for any .NET assembly)
Dependencies
If your plugin references additional NuGet packages or assemblies, ensure they are in the same directory as your plugin DLL or in a location resolvable by the .NET runtime. The bridge loads your assembly with `Assembly.LoadFrom()`, which probes the assembly's directory for dependencies.
Debugging Tips
1. **Attach to TCC** â In Visual Studio, use **Debug â Attach to Process** and attach to the running `tcc.exe` process. Set breakpoints in your plugin code.
2. **Console output** â `Console.WriteLine()` writes to TCC's standard output, which is useful for diagnostics.
3. **Error reporting** â If `Initialize()` returns `false`, TCC displays an error and unloads your plugin. Check TCC's output for bridge-level error messages.
4. **Plugin info check** â Use `PLUGIN /I YourPluginName` from the TCC command line to verify that your metadata and function list are correct.
5. **Reload cycle** â Unload and reload during development:
plugin /u MyPlugin
plugin /l C:\Dev\MyPlugin\bin\Debug\MyPlugin.dll
Naming Rules
| Item | Rule |
|-------------------------|---------------------------------------------------------------------------|
| Internal command names | Alphanumeric, **maximum 12 characters** |
| Variable function names | **Maximum 31 characters** (excluding `@` prefix) |
| Internal variable names | Must start with `_` |
| Plugin `Name` | Used as the identifier in `PLUGIN` commands and `$`-qualified invocations |
Exception Handling
TCC wraps all plugin invocations in an exception handler. If your method throws an unhandled exception, TCC catches it and returns `0xFEDCBA98` (not handled). Your plugin will not crash TCC, but the user will receive no output. Use try/catch in your methods for graceful error handling:
C#
public int MYCOMMAND(StringBuilder args)
{
try
{
// your logic here
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"MYCOMMAND error: {ex.Message}");
return 1;
}
}
API Reference
`TCCPluginInfo` Properties
Property |
Type |
Description |
`Name` |
`string` |
Short name displayed by TCC (defaults to assembly filename if empty) |
`Author` |
`string` |
Author name |
`Email` |
`string` |
Contact email |
`WWW` |
`string` |
Website URL |
`Description` |
`string` |
Brief description |
`Functions` |
`string` |
Comma-delimited list of features (see [The Functions String](#the-functions-string)) |
`Major` |
`int` |
Major version |
`Minor` |
`int` |
Minor version |
`Build` |
`int` |
Build number |
`ITCCPlugin` Methods
Method |
Returns |
Description |
`GetPluginInfo()` |
`TCCPluginInfo` |
Provide plugin metadata |
`Initialize()` |
`bool` |
One-time setup; return `true` to continue loading |
`Shutdown(bool endProcess)` |
`bool` |
Cleanup before unload |
Feature Method Signatures
Signature |
Use Case |
`int Method(StringBuilder args)` |
Commands, variables, functions (read-write) |
`int Method(string args)` |
Commands (read-only) |
`int Method()` |
Commands or variables with no arguments |