ABTSoftware / SciChart.JS.Examples

MIT License
85 stars 37 forks source link

Advanced Licensing Java Implementation #213

Open jonthemonke opened 7 months ago

jonthemonke commented 7 months ago

Issue

In an attempt to wrap the Scichart Server Licensing shared object using java (JNA), I ended up with strange behavior when calling any function which returns char *. In Java, the returned pointer memory does not start at the correct offset which holds the string value and therefore invalid characters are returned.

Hypothesis

My guess, after evaluation, is that either the function is returning some std::string object, or that it is actually returning say str.c_str() where str is an std::string. The issue is that returning str.c_str() causes the reference to be dropped once it is out of scope, this means that a dangling pointer is returned. I ended up needing to wrap the function calls in another extern C layer to get the desired functionality in Java, see below.

scichart.h

#include<cstring>
#include <string>

namespace SciChart {
enum class SCRTLicenseType {
  /// Invalid but informs the user a trial is being requested.
  LICENSE_TYPE_NO_LICENSE = 0,
  /// Trial - Valid but with trial notices
  LICENSE_TYPE_TRIAL = 0x02,
  /// Community - Watermark but no expiry
  LICENSE_TYPE_COMMUNITY = 0x03,
  /// Full - Valid
  LICENSE_TYPE_FULL = 0x20,
  /// Full expired - For Non-perpetual (web)
  LICENSE_TYPE_FULL_EXPIRED = 0x04,
  /// Trial expired - Invalid
  LICENSE_TYPE_TRIAL_EXPIRED = 0x40,
  /// Subscription expired - build is after expiry date
  LICENSE_TYPE_SUBSCRIPTION_EXPIRED = 0x80,
  /// Invalid developer license - Invalid machine specific license
  LICENSE_TYPE_INVALID_DEVELOPER_LICENSE = 0x0F,
  /// License that requires server validation
  LICENSE_TYPE_REQUIRES_VALIDATION = 0x2F,
  /// Invalid license - Invalid runtime license
  LICENSE_TYPE_INVALID_LICENSE = 0xFF
};

namespace LicenseServer {

void ResetRuntimeLicense();

bool SetAssemblyName(const std::string &_assemblyName);

/// Sets the Runtime License ( narrow string version ).
/// Returns true passed license key is valid; otherwise false.
bool SetRuntimeLicenseKey(const std::string &_strKey);

/// Gets a type of the Runtime License.
SCRTLicenseType GetLicenseType();

/// Determines whether the Runtime License is valid.
bool CheckLicenseValid();

/// Gets the OrderId of the current license
std::string GetOrderId();

/// Gets the reason for the license failure
std::string GetLicenseErrors();

/// Decode and check challenge.  Generate response with encrypted expiry
std::string ValidateChallenge(const std::string &_challenge);

/// Dumps the information of the Runtime License to returned string.
std::string Dump();

} // namespace LicenseServer
} // namespace SciChart

#ifdef __cplusplus
extern "C" {
#endif
bool SciChartSCS_SetRuntimeLicenseKey(char* _strKey);
#ifdef __cplusplus
}
#endif

#ifdef __cplusplus
extern "C" {
#endif
char *SciChartSCS_GetLicenseErrors();
#ifdef __cplusplus
}
#endif

#ifdef __cplusplus
extern "C" {
#endif
char *SciChartSCS_ValidateChallenge(char* _challenge);
#ifdef __cplusplus
}
#endif

scichart.cpp

#include <string>
#include <cstring>
#include "scichart.h"

extern "C" {
bool SciChartSCS_SetRuntimeLicenseKey(char *_strKey) {
  using namespace std;
  return SciChart::LicenseServer::SetRuntimeLicenseKey(_strKey);
}
}

extern "C" {
char *SciChartSCS_GetLicenseErrors() {
  using namespace std;
  std::string val = SciChart::LicenseServer::GetLicenseErrors();

  char *ret = (char *)malloc(val.size() + 1);
  strcpy(ret, val.c_str());

  return ret;
}
}

extern "C" {
char *SciChartSCS_ValidateChallenge(char *_challenge) {
  using namespace std;
  std::string val = SciChart::LicenseServer::ValidateChallenge(_challenge);

  char *ret = (char *)malloc(val.size() + 1);
  strcpy(ret, val.c_str());

  return ret;
}
}

int main() { return 0; }

I then free the malloc on the Java side to get this to work. I believe that if the current shared object actually wrote it's c_str() to a native char* buffer then returned it, there would be no dangling pointer on the Java side and we would not need to to any additional wrapping.

antichaosdb commented 6 months ago

Thank you for your helpful feedback. You are right about what the code is doing. I was trying to simplify the API to make it easier to integrate from multiple languages. I worried that the danger of returning a buffer is that the calling code does not free it. That's still better than mangled strings though.

The other option is to implement the CSharp api that is generated by SWIG. This involves registering a callback like this (this is C# code but the java should be very similar)

  protected class SWIGStringHelper {

    public delegate string SWIGStringDelegate(string message);
    static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString);

    [global::System.Runtime.InteropServices.DllImport("SciChartLicenseServer", EntryPoint="SWIGRegisterStringCallback_SciChartLicenseServer")]
    public static extern void SWIGRegisterStringCallback_SciChartLicenseServer(SWIGStringDelegate stringDelegate);

    static string CreateString(string cString) {
      return cString;
    }

    static SWIGStringHelper() {
      SWIGRegisterStringCallback_SciChartLicenseServer(stringDelegate);
    }
  }

Then call CSharp_ValidateChallenge instead of ValidateChallenge.

Routing the string creation back to the calling code somehow avoids the need to malloc and free the buffer directly. If you think the callback method is just a better solution overall I will do a cleaner version of it for non C# people.

It would be extremely helpful if you could also provide the java code that calls this so I can test any changes directly.

David Burleigh CTO SciChart

antichaosdb commented 6 months ago

In getting this to work for node.js I have been recommended to use https://www.npmjs.com/package/ffi-rs. Their examples include this for returning strings, which is exactly as you propose.

extern "C" const char *concatenateStrings(const char *str1, const char *str2) {
  std::string result = std::string(str1) + std::string(str2);
  char *cstr = new char[result.length() + 1];
  strcpy(cstr, result.c_str());
  return cstr;
}

With no special handling needed on the consuming side. Hopefully ffi-rs is freeing the returned buffer. I'm updating the library using this pattern now.

jonthemonke commented 6 months ago

Thanks for the responses! I was able to use the CSharp variation but would prefer to use the returned buffer instead so that we don't have to set the callback every time we switch to a new function in order to handle the responses differently.

Another idea I worked through yesterday was creating "Unsafe" and "Free" prefixed variations of the functions for times where someone wants to be explicit about freeing memory by calling C code, instead of relying on Java or node.js libraries to handle the free, see below:

scichart.h

#include <cstring>
#include <string>

namespace SciChart {
enum class SCRTLicenseType {
  /// Invalid but informs the user a trial is being requested.
  LICENSE_TYPE_NO_LICENSE = 0,
  /// Trial - Valid but with trial notices
  LICENSE_TYPE_TRIAL = 0x02,
  /// Community - Watermark but no expiry
  LICENSE_TYPE_COMMUNITY = 0x03,
  /// Full - Valid
  LICENSE_TYPE_FULL = 0x20,
  /// Full expired - For Non-perpetual (web)
  LICENSE_TYPE_FULL_EXPIRED = 0x04,
  /// Trial expired - Invalid
  LICENSE_TYPE_TRIAL_EXPIRED = 0x40,
  /// Subscription expired - build is after expiry date
  LICENSE_TYPE_SUBSCRIPTION_EXPIRED = 0x80,
  /// Invalid developer license - Invalid machine specific license
  LICENSE_TYPE_INVALID_DEVELOPER_LICENSE = 0x0F,
  /// License that requires server validation
  LICENSE_TYPE_REQUIRES_VALIDATION = 0x2F,
  /// Invalid license - Invalid runtime license
  LICENSE_TYPE_INVALID_LICENSE = 0xFF
};

namespace LicenseServer {

void ResetRuntimeLicense();

bool SetAssemblyName(const std::string &_assemblyName);

/// Sets the Runtime License ( narrow string version ).
/// Returns true passed license key is valid; otherwise false.
bool SetRuntimeLicenseKey(const std::string &_strKey);

/// Gets a type of the Runtime License.
SCRTLicenseType GetLicenseType();

/// Determines whether the Runtime License is valid.
bool CheckLicenseValid();

/// Gets the OrderId of the current license
std::string GetOrderId();

/// Gets the reason for the license failure
std::string GetLicenseErrors();

/// Decode and check challenge.  Generate response with encrypted expiry
std::string ValidateChallenge(const std::string &_challenge);

/// Dumps the information of the Runtime License to returned string.
std::string Dump();

} // namespace LicenseServer
} // namespace SciChart

#ifdef __cplusplus
extern "C" {
#endif
bool SciChartSCS_SetRuntimeLicenseKey(char *_strKey);
char *Unsafe_SciChartSCS_ValidateChallenge(char *_challenge);
char *Unsafe_SciChartSCS_GetLicenseErrors();
void Free_SciChartSCS_ValidateChallenge();
void Free_SciChartSCS_GetLicenseErrors();
#ifdef __cplusplus
}
#endif

scichart.cpp

#include "scichart.h"
#include <cstring>
#include <string>

extern "C" {

using namespace std;
static char *ret_license_errors = NULL;
static char *ret_license_key = NULL;

bool SciChartSCS_SetRuntimeLicenseKey(char *_strKey) {
  return SciChart::LicenseServer::SetRuntimeLicenseKey(_strKey);
}

char *Unsafe_SciChartSCS_GetLicenseErrors() {
  std::string val = SciChart::LicenseServer::GetLicenseErrors();

  ret_license_errors = (char *)malloc(val.size() + 1);
  strcpy(ret_license_errors, val.c_str());

  return ret_license_errors;
}

void Free_SciChartSCS_GetLicenseErrors() {
  if (ret_license_errors != NULL) {
    free(ret_license_errors);
    ret_license_errors = NULL;
  }
}

char *Unsafe_SciChartSCS_ValidateChallenge(char *_challenge) {
  std::string val = SciChart::LicenseServer::ValidateChallenge(_challenge);

  ret_license_key = (char *)malloc(val.size() + 1);
  strcpy(ret_license_key, val.c_str());

  return ret_license_key;
}

void Free_SciChartSCS_ValidateChallenge() {
  if (ret_license_key != NULL) {
    free(ret_license_key);
    ret_license_key = NULL;
  }
}
}

int main() { return 0; }

The user can call the "Free" prefixed functions in runtime and allow the C to handle the free.

Also, see below for how it is being used on the java side. I am just stubbing this out right now so there is minimal functionality.

App.java

    public static void main(String[] args) {
        try {
            // Option 1 - original wrapped methods
            // // set the license key
            // String licenseKey = "yourLicenseKeyHere";
            // int result = SciChartSCS.SciChartSCSLibrary.INSTANCE.SciChartSCS_SetRuntimeLicenseKey(licenseKey);
            // System.out.println("Result: " + result);
            //
            // Pointer pointer = SciChartSCS.SciChartSCSLibrary.INSTANCE.SciChartSCS_GetLicenseErrors();
            // String errors = pointer.getString(0, StandardCharsets.UTF_8.name());
            // System.out.println("Errors: " + errors);
            // Native.free(Pointer.nativeValue(pointer));
            //
            // Pointer validation = SciChartSCS.SciChartSCSLibrary.INSTANCE
            //         .SciChartSCS_ValidateChallenge("yourChallengeHere");
            // System.out.println("Validation: " + validation.getString(0, StandardCharsets.UTF_8.name()));
            // Native.free(Pointer.nativeValue(validation));

            // Option 2 - using callbacks - does not require freeing memory or additional C wrappers, but requires
            // additional Java code and a callback interface. All string results are written to the same callback.
            StringCallback callback = new ImplStringCallback();

            SciChartLicenseServer.SciChartLibrary.INSTANCE
                    .SWIGRegisterStringCallback_SciChartLicenseServer(callback);

            SciChartLicenseServer.SciChartLibrary.INSTANCE.CSharp_ValidateChallenge("yourChallengeHere");

            // Option 3 - unsafe methods - user can free memory manually, open to any language
            // set the license key
            String licenseKey = "yourLicenseKeyHere";
            int result = SciChartSCSNew.SciChartSCSNewLibrary.INSTANCE.SciChartSCS_SetRuntimeLicenseKey(licenseKey);
            System.out.println("Result: " + result);

            Pointer pointer = SciChartSCSNew.SciChartSCSNewLibrary.INSTANCE.Unsafe_SciChartSCS_GetLicenseErrors();
            String errors = pointer.getString(0, StandardCharsets.UTF_8.name());
            System.out.println("Errors: " + errors);
            SciChartSCSNew.SciChartSCSNewLibrary.INSTANCE.Free_SciChartSCS_GetLicenseErrors();

            Pointer validation = SciChartSCSNew.SciChartSCSNewLibrary.INSTANCE
                    .Unsafe_SciChartSCS_ValidateChallenge("yourChallengeHere");
            System.out.println("Validation: " + validation.getString(0, StandardCharsets.UTF_8.name()));
            SciChartSCSNew.SciChartSCSNewLibrary.INSTANCE.Free_SciChartSCS_ValidateChallenge();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

SciChartSCSNew.java

public class SciChartSCSNew {

    private static SciChartSCSNewLibrary getLibrary() {
        String libraryName = "SciChartSCSNew";
        String libraryPath = "/tmp/native-lib/";
        NativeLibrary.addSearchPath(libraryName, libraryPath);
        SciChartSCSNewLibrary lib = (SciChartSCSNewLibrary) Native.load(
                libraryName,
                SciChartSCSNewLibrary.class);
        return lib;
    }

    public interface SciChartSCSNewLibrary extends Library {
        SciChartSCSNewLibrary INSTANCE = SciChartSCSNew.getLibrary();

        int SciChartSCS_SetRuntimeLicenseKey(String key);

        Pointer Unsafe_SciChartSCS_GetLicenseErrors();
        void Free_SciChartSCS_GetLicenseErrors();

        Pointer Unsafe_SciChartSCS_ValidateChallenge(String challenge);
        void Free_SciChartSCS_ValidateChallenge();
    }
}