How to write a wrapper?

Started by Midimaster, April 01, 2021, 14:59:29

Previous topic - Next topic

Midimaster

*** EDIT ***
UPDATE

There is a executable version of the wrapper in a separate topic..: Source Code, Examples and Download!!!


https://www.syntaxbomb.com/index.php/topic,8419.0.html





I want to learn to write a wrapper in BlitzMax. I have no idea about C, but I'm enthusiastic and have a lot of time. I also would like to write down my experiences in a tutorial or worklog.

I decided not to start with a simple example but with a code moster of 65.000 code lines: Final target is to write the wrapper for "Miniaudio.h".


I cannot find any tutorial about wrappers and the BlitzMax manual tells only little about interacting with C.
Can someone point me to some more informations?

My current question is:
The first code line in an example in C is:
    int main()
    {
        ma_device_config config = ma_device_config_init(ma_device_type_playback);


and the function ma_device_config_init:
MA_API ma_device_config ma_device_config_init(ma_device_type deviceType)
{
    ma_device_config config;
    MA_ZERO_OBJECT(&config);
    config.deviceType = deviceType;
...
    Return config;
}



Now I though i can call this function with a simple:

BlitzMax-Code 1
Code (BlitzMax) Select
Import "miniaudio.h"

Extern
Function ma_device_config_init(1)
End Extern


or
BlitzMax-Code 2
Code (BlitzMax) Select
Import "miniaudio.h"

Const ma_device_type_playback:Int = 1
Const ma_device_type_capture:Int  = 2

Extern
Function ma_device_config_init(ma_device_type_playback)
End Extern


but in the MiniAudio.h the ma_device_type is a Enum???
typedef Enum
{
    ma_device_type_playback = 1,
    ma_device_type_capture  = 2,
    ma_device_type_duplex   = ma_device_type_playback | ma_device_type_capture, /* 3 */
    ma_device_type_loopback = 4
} ma_device_type;



And the error messages are for BlitzMax-Code 1:
QuoteSyntax error - expecting identifier, but found '1'

and for BlitzMax-Code 2:
QuoteIn file included from C:/BasicNG/FreeAudio/.bmx/MiniAudio.bmx.gui.debug.win32.x64.c:1:
C:/BasicNG/FreeAudio/.bmx/MiniAudio.bmx.gui.debug.win32.x64.h:56:14: error: conflicting types for 'ma_device_config_init'
extern BBINT ma_device_config_init(BBINT bbt_ma_device_type_playback);
              ^~~~~~~~~~~~~~~~~~~~~
In file included from C:/BasicNG/FreeAudio/.bmx/MiniAudio.bmx.gui.debug.win32.x64.h:54,
                 from C:/BasicNG/FreeAudio/.bmx/MiniAudio.bmx.gui.debug.win32.x64.c:1:
C:/BasicNG/FreeAudio/miniaudio.h:4773:25: note: previous declaration of 'ma_device_config_init' was here
MA_API ma_device_config ma_device_config_init(ma_device_type deviceType);
                         ^~~~~~~~~~~~~~~~~~~~~


So how to send the parameter "1" to the C-function?

by the way... here is a link to the complete MiniAudio.h and it's homepage:


anybody who wants to join the project? Helpers are welcome.

...on the way to Egypt

Scaremonger

#1
Brucey is the king of wrapping, but I have had a little dabble when I find some code I want to integrate into a project.

There is a post here where Brucey helped me with some code and gives some useful advice.

I found that the biggest hurdle was identifying which Blitzmax variable type should be used for a particular C/C++ variable type. Int, and long are usually easy, but others are not quite so obvious.

For example, the following C code is a simple function that does not return anything (void). The variable however caused me a lot of grief at first:


void Display(char *str) {
    printf(str);
}


This can be wrapped without any glue code into something like this:


Import "display.c"

Extern "C"
    Function Display( str$z )
End Extern

Display "Hello World!"


In your example, ma_device_config_init() appears to return a variable of type ma_device_config. So it would be more like:


Const ma_device_type_playback:int = 1

extern "c"
  function ma_device_config_init:ma_device_config( deviceType:int )
End extern

Local config:ma_device_config = ma_device_config_init( ma_device_type_playback )


You'll notice that I have not used a BlitzMaxNG Enum and gone for a simple constant of type int. I think it makes the code more readable bit that's just my opinion.

The return type ma_device_config is probably a struct or a pointer to a buffer.

Midimaster

#2
Thanks for helping me

MiniAudio-Wrapper

Yes there are two problems at the same time. I need to know how to pass this INTEGER value to a C-function which expects a ENUM "ma_device_type". And at the same time I have to offer a STRUCT "ma_device_config" where the function can return something.

So my second question would be whether this STRUCT can be simulated with TYPE in BlitzMax?



At the same time I work on a thereotic wrapper to learn all about possible parameters and return values.

Learn-Wrapper

I already know how to handle simple types like INTEGER or STRING. for learning purpose I write a C-file and the corresponing wrapper. This is the current state:

The "library": learn.c
// example for INTEGER send and return
int Doubler( int x ){
return x+x;
}

// example for STRING send
void Display(char *str) {
    printf(str);
}

// example for STRING send and return
char *Question(char *str) {
strcat(str, "?" );
    return str;
}



The Wrapper: learn.bmx
Code (BlitzMax) Select
Import "learn.c"

Global LearnC:Wrapper=New Wrapper

Print LearnC.Doubler(33)
      LearnC.Display("chers")
Print
Print LearnC.Question("abc")

Type Wrapper
Function Doubler:Int (Value:Int)
Return _Doubler(Value)
End Function

Function Display(Text:String)
_Display ( Text )
End Function

Function Question:String(Text:String)
Return String.FromCString( _Question ( Text ) )
End Function
End Type



Extern
Function _Doubler( x ) ="Doubler"

Function _Display( str$z) = "Display"

Function _Question:Byte Ptr(str$z) ="Question"

End Extern



Now I have to find out, why this works with a C-File with the extension "learn.c" but not with "learn.h"
...on the way to Egypt

col

QuoteNow I have to find out, why this works with a C-File with the extension "learn.c" but not with "learn.h"

You need to learn c to know why this is:-

Header files are not designed to be compiled as a standalone compilation unit and always require a c/cpp file to pull them in to that file - even in the C world of programming. This doesn't mean you cannot have executable code in a header - quite the contrary - but you do need a .c file to bring the header in to be compiled.

Where you see a #include "something.h" in a c/c++ source then the contents of that .h file are literally pasted into the c/cpp source file by the pre-processor before that c/cpp file is compiled - in the same way that in BMax that Include "whatever.bmx" is pulled into the main .bmx file.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

#4
Some datatypes translate directly between BMax and C, some don't.

You can these directly from BMax <--> c/cpp.
Int <--> int
Float <--> float
Byte <--> char
String.ToCString (or $z) --> char*
String.FromCString (or $z) <-- char*
String.ToWString (or $w) --> wchat_t*
String.FromWString (or$w) <-- wchar_t*

After getting used to passing data using those types between the languages you will also need to understand what pointers are - especially from the c/cpp side. When you create an instance of a struct/class in c/cpp you would use malloc/new. The returned value is a pointer value. The value will be a memory address on the heap. At that memory address is where the struct/class data is actually stored - hence the value 'points' to where the object data (the instance) really is. You can create those in the c/c++ source (eg an instance of the 'MiniAudio' struct and store it in a BMax program as a Byte Ptr. To keep thing clean in the BMax side you would create a Type called MiniAudio with a field such as pMiniAudio:Byte Ptr. From BMax you would call a c function that will return the pointer to the c instance and store in Byte Ptr field.

The following is an example of create a c object and 'wrapping' it in a BMax object. In reality this would be fleshed out with data members and methods/functions to take advantage of what you are wrapping.

Anyway a simple skeleton example of wrapping a basic c struct in BMax:

Bmax code

Import "MiniAudio.c"

Extern"c"
Function MiniAudio_Create:Byte Ptr()
Function MiniAudio_Delete(pMiniAudio:Byte Ptr)
Function MiniAudio_SetValue(pMiniAudio:Byte Ptr, Value:Int)
Function MiniAudio_GetValue:Int(pMiniAudio:Byte Ptr)
EndExtern

Type MiniAudio
   Method Create:MiniAudio()
      pMiniAudio = MiniAudio_Create()
      Return Self
   EndMethod

   Method Delete()
      If pMiniAudio
         MiniAudio_Delete(pMiniAudio)
      EndIf
   EndMethod

   Method SetValue(Value:Int)
      MiniAudio_SetValue(pMiniAudio, Value)
   EndMethod

   Method PrintValue()
      Print(MiniAudio_GetValue(pMiniAudio))
   EndMethod

   Field pMiniAudio:Byte Ptr
EndType

Local MA:MiniAudio = New MiniAudio.Create()
MA.SetValue(100)
MA.PrintValue()



C code


// put into MiniAudio.h
struct MiniAudio {
   int Value;
};

// Put into MiniAudio_Glue.c
struct MiniAudio *MiniAudio_Create() {
   struct MiniAudio *pMiniAudio;
   pMiniAudio = (struct MiniAudio*)malloc(sizeof(struct MiniAudio));
   pMiniAudio->Value = 0;
   return pMiniAudio;
}

void MiniAudio_Delete(struct MiniAudio *pMiniAudio) {
   free(pMiniAudio);
}

void MiniAudio_SetValue(struct MiniAudio *pMiniAudio, int Value) {
   pMiniAudio->Value = Value;
}

int MiniAudio_GetValue(struct MiniAudio *pMiniAudio) {
   return pMiniAudio->Value;
}

https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

#5
If you encounter an array in c/cpp (not a std::array from cpp) then these are contiguous areas of memory in c/cpp. In Bmax an array is a pointer to the BMax array object - Totally different types from each other. We can cross that bridge when it comes ;)

Did I mention that you should really learn the language of what you are wrapping - c/cpp :)
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Midimaster

Wow, thank you... thats a lot of information.

As you mentioned, it will help to know the language C. That one of the targets of this project. I started to to exactly this, but always with the target of using the new experience for optimizing my BlitzMax skills.

One of my priciples ist "never to use any piece of code" only to quickly contuniue, even when I did not understand the code. But understand deeply what happens under the hood.

So this C is a last miracle for me and know it has to be explored. And with a little help of others, who point me to the next questions or answers, i could be done.

With your post I have to do for hours and will have a lot of fun with it. I will report, what I found out and where are still some questions...

...on the way to Egypt

Derron

BlitzMax NG also has structs.
C code often has "integers" or "pointers" so it might be rather easy to wrap "simple" stuff.

But bigger libraries ... you often have to code a lot of stuff until you can do some "tests" - which makes it not easy to "code along".

Happy tinkering.

bye
Ron

Midimaster

#8
Quote from: col on April 02, 2021, 06:26:21

You can these directly from BMax <--> c/cpp.
..
String.ToCString (or $z) --> char*
String.FromCString (or $z) <-- char*

Ok, I already wondered, what this "$z" is... So this is both possible:
Code (BltzMax) Select
Extern
   Function _DisplayA( str:Byte Ptr ) = "Display"
   Function _DisplayB( str$z ) = "Display"
EndExtern

Function DisplayA(Text:String)
_DisplayA ( Text.ToCString() )
End Function

Function DisplayB(Text:String)
_DisplayB ( Text )
End Function



Why "code.c" and "code.h"

As MiniAudio is only one single "minisound.h"-File I need a empty "*.c"-file to use it in BlitzMax? I tried this with my "learn.h" and it works:

"learn.c"
#include "learn.h"


"learn.h"
// example for INTEGER send and return
int Doubler( int x ){
return x+x;
}




"learn.bmx"
Code (BlitzMax) Select
SuperStrict
Import "learn.c"

Doubler(33)

Extern
Function Doubler:Int( x:Int )
End Extern




Writing part of the wrapperr in C?

The STRUCT for the ma_device_config is mega-complicate and the BlitzMax OP user only needs to change 3, 4 parameters in it. So I think it would be a good idea to write some C additional functions in a "MiniAudio.c"-file to have full access to the STRUCT and ENUMS and on the oher side to offer only very small (and harmless) access from BlitzMax side?

This is all you would need for a main() if it is in C:

Code (C#) Select
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount)
{ // some code}

int main()
{
    ma_device_config config = ma_device_config_init(ma_device_type_playback);
    config.playback.format   = ma_format_f32;   // Set to ma_format_unknown to use the device's native format.
    config.playback.channels = 2;               // Set to 0 to use the device's native channel count.
    config.sampleRate        = 48000;           // Set to 0 to use the device's native sample rate.
    config.dataCallback      = data_callback;   // This function will be called when miniaudio needs more data.
    config.pUserData         = pMyCustomData;   // Can be accessed from the device object (device.pUserData).

    ma_device device;
    if (ma_device_init(NULL, &config, &device) != MA_SUCCESS) {
        return -1;  // Failed to initialize the device.
    }

    ma_device_start(&device);     // The device is sleeping by default so you'll need to start it manually.

    // Do something here. Probably your program's main loop.

    ma_device_uninit(&device);    // This will stop the device so no need to do that manually.
    return 0;
}


but the struct is:
struct ma_device_config
{
    ma_device_type deviceType;
    ma_uint32 sampleRate;
    ma_uint32 periodSizeInFrames;
    ma_uint32 periodSizeInMilliseconds;
    ma_uint32 periods;
    ma_performance_profile performanceProfile;
    ma_bool8 noPreZeroedOutputBuffer;   /* When set to true, the contents of the output buffer passed into the data callback will be left undefined rather than initialized to zero. */
    ma_bool8 noClip;                    /* When set to true, the contents of the output buffer passed into the data callback will be clipped after returning. Only applies when the playback sample format is f32. */
    ma_device_callback_proc dataCallback;
    ma_stop_proc stopCallback;
    void* pUserData;
    struct
    {
        ma_resample_algorithm algorithm;
        struct
        {
            ma_uint32 lpfOrder;
        } linear;
        struct
        {
            int quality;
        } speex;
    } resampling;
    struct
    {
        const ma_device_id* pDeviceID;
        ma_format format;
        ma_uint32 channels;
        ma_channel channelMap[MA_MAX_CHANNELS];
        ma_channel_mix_mode channelMixMode;
        ma_share_mode shareMode;
    } playback;
    struct
    {
        const ma_device_id* pDeviceID;
        ma_format format;
        ma_uint32 channels;
        ma_channel channelMap[MA_MAX_CHANNELS];
        ma_channel_mix_mode channelMixMode;
        ma_share_mode shareMode;
    } capture;

    struct
    {
        ma_bool8 noAutoConvertSRC;     /* When set to true, disables the use of AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM. */
        ma_bool8 noDefaultQualitySRC;  /* When set to true, disables the use of AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY. */
        ma_bool8 noAutoStreamRouting;  /* Disables automatic stream routing. */
        ma_bool8 noHardwareOffloading; /* Disables WASAPI's hardware offloading feature. */
    } wasapi;
    struct
    {
        ma_bool32 noMMap;           /* Disables MMap mode. */
        ma_bool32 noAutoFormat;     /* Opens the ALSA device with SND_PCM_NO_AUTO_FORMAT. */
        ma_bool32 noAutoChannels;   /* Opens the ALSA device with SND_PCM_NO_AUTO_CHANNELS. */
        ma_bool32 noAutoResample;   /* Opens the ALSA device with SND_PCM_NO_AUTO_RESAMPLE. */
    } alsa;
    struct
    {
        const char* pStreamNamePlayback;
        const char* pStreamNameCapture;
    } pulse;
    struct
    {
        ma_bool32 allowNominalSampleRateChange; /* Desktop only. When enabled, allows changing of the sample rate at the operating system level. */
    } coreaudio;
    struct
    {
        ma_opensl_stream_type streamType;
        ma_opensl_recording_preset recordingPreset;
    } opensl;
    struct
    {
        ma_aaudio_usage usage;
        ma_aaudio_content_type contentType;
        ma_aaudio_input_preset inputPreset;
    } aaudio;
};

...on the way to Egypt

Henri

#9
I don't think you need to duplicate miniaudio structures 1 on 1. Easiest way (what col probably suggested) is just to create these in C-code and return them in a Byte Ptr. For this you would create an intermediary C-file called glue.c/cpp (or similar)

Example of creating an instance and using it (taking from my Excel wrapper):

Code (c) Select

'//glue.cpp
'-----------
#include <iostream>
#include <cstdio>
#include <conio.h>
#include "include_cpp/libxl.h"
#include <brl.mod/blitz.mod/blitz.h>

//This is a C++ keyword and doesn't work in C. Just makes typing easier
using namespace libxl;

extern "C"
{

//New XML book
Book* xlNewXMLBook()
{
//This creates an instance. xlCreateXMLBook is just a convenience function provided by LibXL
    Book* book = xlCreateXMLBook();
    if(book)
{
book->setKey("Henri", "my_secret_licence_key");
return book;
}
return 0;
}
//Add new sheet
Sheet* xlAddSheet(Book* book, char* name)
{
Sheet* sheet = book->addSheet(name);
    if(sheet)
return sheet;
else
return 0;
}

} //End extern block. Wouldn't it be convenient if I could call this End Extern so I wouldn't have to write this long comment :-)



Code (blitzmax) Select

//myLib.bmx
'------------
Extern "C"
Function xlNewBook:Byte Ptr()
Function xlAddSheet:Byte Ptr(book:Byte Ptr, name$z)
EndExtern

'Using it in Blitzmax. This would probably be better contained inside a Type to make it neat
'--------------------------------------------------------------------------------------------
Local book:Byte Ptr = xlNewBook()
Local sheet:Byte Ptr = xlAddSheet(book, "My_sheet")
If sheet Then Print "Succesfully created sheet for my Excel"


Note:
z after string variable $z means that the memory reserved for the string is freed after use. This would be equal to calling MemFree(<myString Byte Pointer>)

-Henri
- Got 01100011 problems, but the bit ain't 00000001

col

#10
I'm glad my little primer was helpful for you.

QuoteSo this is both possible:
You could do that yes. $z and $w are short hand helpers - Don't use a regular String as that is an Object. It's similar to # for Float, $ for String, % for Int etc.
Declaring (or is it 'defining' in this case?) the name of the function in quotes is also helpful when a different calling convention is used. Binaries produced by a C compiler 'generally' won't have decorated function names unless the calling convention is changed - in the world of BMax you will probably only see cdecl and stdcall. This is a different-ish topic to wrapping but still very relevant to the wrapping process and is something that you will need to know in order to be confident to wrap anything :)

The stdcall convention is what 32bit Windows libraries/dlls use and have an @4 after the name. The '4' is the combined number of bytes for the parameters. So say you have a function defined as

Extern"Win32"
Function DoSomething(ParamA:Int, ParamB:Int)
EndExtern

then you could use

Extern"Win32"
Function DoSomething(ParamA:Int, ParamB:Int) = "DoSomething@8"
EndExtern

The '8' is for 2 x Int in the parameters with an Int being 4 bytes.

There are also tools out there to show you the real name of the function in the binary namely 'https://www.dependencywalker.com/'. There are also plenty of command line tools to examine binary objects to get the same information.



Getting back to wrapping in BMax... I was getting my parties muddled up - my excuse is not working with BMax for quite some time. I'll scratch this out to save confusion.

I can see that you are learning this pretty quickly which is nice, but before we move on to working on wrapping the library there is another way to do this for C structs and POD (Plain Old Data) types in c++.
In my simple example above we could declare the Type within the Extern. For C this is ok as structs don't have methods in the same way that cpp does. This also makes the bridging code significantly easier. One very important detail is that both the BMax type and the c struct MUST be the same size with all of the members lined up correctly bit for bit. The example is now a lot simpler...
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

Before going any further I need to check that last part (where the Type is within the Extern block) - this may be a c++ thing...

I'll check...
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

Yeah scrap that last part  :o It's been a while since I did any of this stuff :D

I was getting confused with filling Types with data as opposed to creating instances of them. My bad!!
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

col

Quotethe BlitzMax OP user only needs to change 3, 4 parameters in it
Heh I was getting carried away with generic wrapping but for your use case it seems much simpler. You could write a one liner function with the parameters as you suggest.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Midimaster

#14
Thank you so much for all this informations. I think at the moment I need it more simple. At first I try to build this Learn-Wrapper. Code-Snipplet from other wrappers are still to complicate for me to understand it right now. I will have a look on it in future.

If you want to help please reduce any code to the level of the Learn-Wrapper.



I now try to simulate a user defined type in c and then in a next step I want to call a c-function which expects this type.

but I fail already in defining the type:

// example for sending a value in a ENUM
void SendEnun(ma_device_type deviceType) {
}

typedef enum
{
    ma_device_type_playback = 1,
    ma_device_type_capture  = 2,
    ma_device_type_duplex   = 3,
    ma_device_type_loopback = 4
} ma_device_type;


I thought this would work... but the error message is:
C:/BasicNG/FreeAudio/learn.h:21:15: error: unknown type name 'ma_device_type'
void SendEnun(ma_device_type deviceType) {
               ^~~~~~~~~~~~~~


What did i forget?


...my wife says we're going for a walk now....
...on the way to Egypt