Go Back   Cockos Incorporated Forums > REAPER Forums > ReaScript, JSFX, REAPER Plug-in Extensions, Developer Forum

Reply
 
Thread Tools Display Modes
Old 09-25-2021, 06:30 PM   #1
gxray
Human being with feelings
 
Join Date: Dec 2020
Location: Miami, FL USA
Posts: 396
Default ReaperSharp - Build native REAPER extensions in C#/VB instead of C++

FAIR WARNING: I've never written C# before this, so the code may not be great.

------------------

In my spare time I have been working on porting the REAPER C/C++ API to common languages. I plan to publish translations and working examples in most common languages, including Java/Kotlin, Swift, and others.

Please let me know if there's a language you'd like to have native access from.

I've managed to port it so far to D and C#.
Though nobody writes D, so I have yet to publish my D translation.


Instructions:
  1. Clone https://github.com/GavinRay97/reaper-native
  2. Install .NET 6 SDK from https://dotnet.microsoft.com/download/dotnet/6.0
  3. Run "dotnet build" from inside of "reaper-native/dotnet"
  4. Copy files from "reaper-native/dotnet/bin/Debug/net6.0/win-x64" to "REAPER/UserPlugins"
  5. Start REAPER, see console message which was fired by C# calling a C/C++ method

What this is:

A starter project which can successfully compile a functioning shared library that works as a REAPER native extension and has access to the C API.

I have translated the definitions of all +800 functions from C, to C#'s unmanaged function pointer syntax.

IE, the following C code:
PHP Code:
void (*ShowConsoleMsg)(const charmsg); 
Can be called like this in C#:
PHP Code:
// NativeTypeName is just for documentation
[NativeTypeName("void (*)(const char *)")]
public 
delegateunmanaged[Cdecl]<sbyte*, voidShowConsoleMsg
Additionally, I've taken care of "loading in" the function pointers at the start of the plugin for you. The C header does this for you with the REAPERAPI_IMPLEMENT macro, but since we're not in C-land, we've no such niceties.

This works via reflection and calculating struct offsets.
Many thanks to "Zombie" from the C# Discord server "#low-level" channel for help here.

After all this, that means you can write a native REAPER extension in .NET with just:
PHP Code:
public static unsafe class MyReaperPlugin
{
  [
UnmanagedCallersOnly(EntryPoint "ReaperPluginEntry")]
  public static 
int ReaperPluginEntry(IntPtr hInstance, [DNNE.C99Type("struct reaper_plugin_info_t*")] reaper_plugin_info_trec)
  {
    try
    {
      
// Initialize
      
reaper_plugin_functions reaper = new reaper_plugin_functions();
      
reaper.Initialize(rec);

      
// Hello World
      
IntPtr msgPtr Marshal.StringToHGlobalAnsi("Hello From C#");
      try
      {
        
reaper.ShowConsoleMsg((sbyte*)msgPtr);
      }
      finally
      {
        
Marshal.FreeHGlobal(msgPtr);
      }

      return 
1;
    }
    
// Handle exception
    
catch (Exception e)
    {
      
System.Diagnostics.Debug.WriteLine(e);
      return 
0;
    }
  }

================================================== ===========

Long explanation that probably nobody cares about:

.NET these days allows you to:
The tl;dr of it, is that we can write something like this:
PHP Code:
[UnmanagedCallersOnly(EntryPoint "add")]
public static 
int Add(int aint b)
{
    return 
b;

And then have a native artifact which exports this:
PHP Code:
DLL_EXPORT int add(int aint b); 
The REAPER native API expects an export name called "ReaperPluginEntry", that it will call with arguments:

PHP Code:
struct reaper_plugin_info_t
{
  
int caller_version;
  
voidhwnd_main;
  
int (*Register)(const charnamevoidinfostruct);
  
void* (*GetFunc)(const charname);
};

int ReaperPluginEntry(HINSTANCEreaper_plugin_info_trec); 
Knowing this, and knowing that we can export C calling-convention functions from .NET, then we can write a C# library which exports the same thing:

PHP Code:
public static unsafe class MyReaperPlugin
{

  private const 
string ReaperPluginInfoStructString = @"
    struct reaper_plugin_info_t
    {
      int caller_version;
      void* hwnd_main;
      int (*Register)(const char* name, void* infostruct);
      void* (*GetFunc)(const char* name);
    };
  "
;

  public 
unsafe partial struct reaper_plugin_info_t
  
{
    public 
int caller_version;

    public 
IntPtr hwnd_main;

    [
NativeTypeName("int (*)(const char *, void *)")]
    public 
delegateunmanaged[Cdecl]<sbyte*, void*, intRegister;

    [
NativeTypeName("void *(*)(const char *)")]
    public 
delegateunmanaged[Cdecl]<sbyte*, void*> GetFunc;
  }

  [
UnmanagedCallersOnly(EntryPoint "ReaperPluginEntry")]
  [
DNNE.C99DeclCode(ReaperPluginInfoStructString)]
  public static 
int ReaperPluginEntry(IntPtr hInstance, [DNNE.C99Type("struct reaper_plugin_info_t*")] reaper_plugin_info_trec)
  {
    try
    {
      
// &ShowConsoleMsg = (ShowConsoleMsg)rec->GetFunc("ShowConsoleMsg")
      
IntPtr fnNamePtr Marshal.StringToHGlobalAnsi("ShowConsoleMsg");
      
delegateunmanaged[Cdecl]<sbyte*, voidShowConsoleMsg;
      try
      {
        
voidfnPtr rec->GetFunc((sbyte*)fnNamePtr);
        
ShowConsoleMessage = (unmanaged[Cdecl]<sbyte*, void>) fnPtr;
      }
      finally
      {
        
Marshal.FreeHGlobal(fnNamePtr);
      }

      
// reaper.ShowConsoleMessage("Hello World")
      
IntPtr msgPtr Marshal.StringToHGlobalAnsi("Hello From C#");
      try
      {
        
ShowConsoleMsg((sbyte*)msgPtr);
      }
      finally
      {
        
Marshal.FreeHGlobal(msgPtr);
      }

      return 
1;
    }
    
// Handle exception
    
catch (Exception e)
    {
      
System.Diagnostics.Debug.WriteLine(e);
      return 
0;
    }
  }

If we take a look at the compiled library exports, you'll see our entrypoint listed, and there's no more magic to it than that:

__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)

Last edited by gxray; 09-26-2021 at 08:39 AM.
gxray is offline   Reply With Quote
Old 09-26-2021, 01:44 AM   #2
X-Raym
Human being with feelings
 
X-Raym's Avatar
 
Join Date: Apr 2013
Location: France
Posts: 9,900
Default

I was wondering what you were up to after your reaimgui exploration :P


Seems like you went pretty far once again ! Well done,.and thx for sharing !
X-Raym is offline   Reply With Quote
Old 09-26-2021, 06:36 AM   #3
Apokalipsis
Human being with feelings
 
Join Date: Jan 2012
Location: Ukraine->Germany
Posts: 60
Default

Gxray,
Great idea to write on C#
Maybe you know how to write Reaper extension in PureBasic?

I tried several times, just to translate my simple code from pure C on PureBasic.
__________________
Alex Longard
Apokalipsis is offline   Reply With Quote
Old 09-26-2021, 09:23 AM   #4
gxray
Human being with feelings
 
Join Date: Dec 2020
Location: Miami, FL USA
Posts: 396
Default

Quote:
Originally Posted by X-Raym View Post
I was wondering what you were up to after your reaimgui exploration :P


Seems like you went pretty far once again ! Well done,.and thx for sharing !
Ha, I still need to publish the reaimgui stuff ;..;

Thanks! This isn't much better at the moment than using C/C++, it needs someone to come through and write a wrapper function for each REAPER API method.

Like:

PHP Code:
class Reaper
{
  public 
void ShowConsoleMsg(string message)
  {
      
IntPtr msgPtr Marshal.StringToHGlobalAnsi(message);
      try
      {
        
reaper.ShowConsoleMsg((sbyte*)msgPtr);
      }
      finally
      {
        
Marshal.FreeHGlobal(msgPtr);
      }
  }

And then you can just do:
PHP Code:
reaper.ShowConsoleMsg("Hello"); 
Instead of needing to manually allocate the string, marshal it, and free the memory when done.

But my hope is that if anyone wants to write extensions in C#, they can use this to understand how it's possible and then continue to write those wrappers.

Quote:
Originally Posted by Apokalipsis View Post
Gxray,
Great idea to write on C#
Maybe you know how to write Reaper extension in PureBasic?

I tried several times, just to translate my simple code from pure C on PureBasic.
Heya -- I have never written any BASIC based language, but from Googling I think it should be something like this roughly?

Code:
; Register is variadic, you can use it for multiple things, it returns an int for success/failure
Procedure Register(Name, Object)

End Procedure

; GetFunc returns a void* that you need to cast as the corresponding function type
Procedure GetFunc(Name)

End Procedure

Structure ReaperPluginInfoT
    caller_version.w
    hwnd_main.l
    *Register
    *GetFunc
End Structure

ProcedureDLL ReaperPluginEntry(hInstance, *rec.ReaperPluginInfoT)
  showConsoleMsg = *rec\GetFunc("ShowConsoleMsg)
  showConsoleMsg("Test")
EndProcedure
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
gxray is offline   Reply With Quote
Old 09-26-2021, 09:44 AM   #5
Apokalipsis
Human being with feelings
 
Join Date: Jan 2012
Location: Ukraine->Germany
Posts: 60
Default

Gxray,
thanks! I will test it.
I writed my code with prototypes for Reaper functions, and my code not work.
__________________
Alex Longard
Apokalipsis is offline   Reply With Quote
Old 09-26-2021, 10:00 AM   #6
Apokalipsis
Human being with feelings
 
Join Date: Jan 2012
Location: Ukraine->Germany
Posts: 60
Default

Gxray,
your code not work...
Thanks for you help
I rewrite my code, it's work normaly!
Code:
PrototypeC GetFunc(name.p-ascii)

PrototypeC.i ShowMessageBox(msg.p-ascii, title.p-ascii, type.l)

Structure reaper_plugin_info_t Align #PB_Structure_AlignC
caller_version.i
hwnd_main.i
*register
; get a generic API function, there many of these defined.
; void * (*GetFunc)(const char *name); // returns 0 if function not found
*GetFunc.GetFunc
EndStructure

ProcedureCDLL.i ReaperPluginEntry(hInstance.i, *rec.reaper_plugin_info_t)
ShowMessage.ShowMessageBox = *rec\GetFunc("ShowMessageBox")

ShowMessage("hello message", "Title message", 0)
ProcedureReturn 1
EndProcedure

; IDE Options = PureBasic 5.72 (Windows - x64)
; ExecutableFormat = Shared dll
; Folding = -
; Executable = C:\REAPER\UserPlugins\reaper_test_1.dll
; DisableDebugger
__________________
Alex Longard

Last edited by Apokalipsis; 09-26-2021 at 10:50 AM.
Apokalipsis is offline   Reply With Quote
Old 09-26-2021, 12:32 PM   #7
gxray
Human being with feelings
 
Join Date: Dec 2020
Location: Miami, FL USA
Posts: 396
Default

Quote:
Originally Posted by Apokalipsis View Post
Gxray,
your code not work...
Thanks for you help
I rewrite my code, it's work normaly!
That code you posted works to call REAPER from PureBasic?
If so: nice! =D
__________________
Seasoned codemonkey
Dunno a thing about making music (here to learn!)
gxray is offline   Reply With Quote
Old 09-27-2021, 02:10 AM   #8
IXix
Human being with feelings
 
Join Date: Jan 2007
Location: mcr:uk
Posts: 3,891
Default

WTF is D???

Nice work. Any chance you could write an English wrapper?
IXix is offline   Reply With Quote
Old 09-27-2021, 02:28 AM   #9
Apokalipsis
Human being with feelings
 
Join Date: Jan 2012
Location: Ukraine->Germany
Posts: 60
Default

Gxray,
yes, it's work. I see your code and fiks two bugs in my code.
You can test it, reaper_test64.dll and source code in attached arhive.
When i have free time i rewrite all functions from reaper_functions.h.
Attached Files
File Type: zip PB_Reaper_api_test.zip (2.4 KB, 104 views)
__________________
Alex Longard
Apokalipsis is offline   Reply With Quote
Old 01-24-2022, 04:54 PM   #10
Mr.X
Human being with feelings
 
Join Date: Jun 2011
Posts: 97
Default Calling from a C# program

I'm probably missing something simple, but how do I call the various functions from another C# program?

I compiled ReaperSharp and threw the compiled output into the UserPlugins folder and it opened the debug console showing "Hello from C#" so I know the plugin works! (Great job, btw)

So now, how do I call the functions from an external C# program?
Mr.X is offline   Reply With Quote
Old 04-01-2023, 11:39 AM   #11
Mr.X
Human being with feelings
 
Join Date: Jun 2011
Posts: 97
Default

Just checking in to see if there's any interest in completing this project? It would be very useful to me if I could get it working...
Mr.X is offline   Reply With Quote
Old 06-03-2023, 04:05 PM   #12
aspiringSynthesisingAlch
Human being with feelings
 
Join Date: Feb 2014
Posts: 309
Default WOW. Working?

Quote:
Originally Posted by gxray View Post
... This isn't much better at the moment than using C/C++, it needs someone to come through and write a wrapper function for each REAPER API method.
Hi! Another game-changing day, stumbling upon other realms to reap more reaper-awesomeness!

Is this still pertinent? Or already achieved?

Am wondering if this is good to go (instead of learning c++ along with lua reaperscript) or if many issues, or if there's arguments AGAINST using this first, as a newcomer to reaper, and reaper-dev...

SEEMS like this could be worth sharing, adding to arsenal, collaborating on... or may be buggy/deprecated/obsolute/superceded...

If there's a DJ Batman, the ReaPack is his batcave

thanks and wows > int 🙏
aspiringSynthesisingAlch is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 06:19 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.