Skip to content

Conversation

@username77
Copy link
Owner

Summary

  • add a shared databento_c library that wraps LiveThreaded behind a C ABI for consumption via P/Invoke
  • define exported enums, handles, and callback signatures in a new databento/c_api.h
  • install the shim library and header alongside the existing C++ artifacts

Testing

  • cmake -S databento-cpp -B databento-cpp/build (fails: download of nlohmann/json blocked by HTTP 403 from proxy)

https://chatgpt.com/codex/tasks/task_e_68f730d338dc83249d0b20f34148eb10

@gemini-code-assist
Copy link

Summary of Changes

Hello @username77, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the interoperability of the databento library by introducing a C ABI shim. This shim wraps the LiveThreaded component, exposing its functionality through a C-compatible interface. This change is crucial for allowing developers using languages like C# or F# to seamlessly integrate with the databento library via P/Invoke, thereby broadening the library's accessibility and potential use cases. The implementation includes a new C API header and a C++ source file that bridges the C and C++ interfaces, along with necessary build system updates for proper compilation and installation.

Highlights

  • C ABI Shim Library: A new shared library, databento_c, has been introduced to provide a C Application Binary Interface (ABI) for the existing C++ LiveThreaded component.
  • P/Invoke Integration: This C ABI shim enables easier integration with other programming languages, particularly those that can utilize P/Invoke (Platform Invoke) mechanisms, such as C# or F#.
  • C API Header: A new header file, databento/c_api.h, defines the exported enumerations, handle types, and callback signatures necessary for interacting with the databento_c library from C-compatible languages.
  • Build System Integration: The CMake build system has been updated to compile, link, and install the new databento_c shared library and its corresponding header file alongside the existing C++ artifacts.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a C ABI shim for P/Invoke integration by adding a databento_c shared library. The changes include a new C API header (c_api.h), its implementation (databento_c.cpp), and updates to CMakeLists.txt to build and install the new library and header.

The overall approach is solid. The C API is well-designed with opaque pointers for handles, clear function naming, and robust error handling using try-catch blocks and thread-local error messages. Memory management for handles across the C/C++ boundary is handled correctly.

My review focuses on improving maintainability and robustness in the C++ implementation of the shim. I've suggested making enum conversions safer, improving error messages for invalid arguments, and simplifying some of the object construction logic. These changes will make the code easier to maintain and less prone to subtle bugs in the future.

Comment on lines +30 to +41
std::vector<std::string> CopyStrings(const char* const* strings, std::size_t count) {
std::vector<std::string> result;
result.reserve(count);
for (std::size_t i = 0; i < count; ++i) {
const char* value = strings ? strings[i] : nullptr;
if (value == nullptr) {
throw std::invalid_argument("symbol list contains a null entry");
}
result.emplace_back(value);
}
return result;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of CopyStrings is a bit dense. It handles both a null strings pointer and null elements within the strings array with the same check and error message. This can be confusing for debugging.

Refactoring for clarity by handling these cases separately with more specific error messages would improve maintainability.

std::vector<std::string> CopyStrings(const char* const* strings, std::size_t count) {
  if (count > 0 && !strings) {
    throw std::invalid_argument("symbol list is null but count is non-zero");
  }
  std::vector<std::string> result;
  result.reserve(count);
  for (std::size_t i = 0; i < count; ++i) {
    const char* value = strings[i];
    if (value == nullptr) {
      throw std::invalid_argument("symbol list contains a null entry");
    }
    result.emplace_back(value);
  }
  return result;
}

Comment on lines +53 to +63
databento::Schema ToSchema(db_c_schema_t schema) {
return static_cast<databento::Schema>(schema);
}

databento::SType ToSType(db_c_stype_t stype) {
return static_cast<databento::SType>(stype);
}

databento::Dataset ToDataset(db_c_dataset_t dataset) {
return static_cast<databento::Dataset>(dataset);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The functions ToSchema, ToSType, and ToDataset use static_cast to convert from C enums to C++ enums. This is brittle because it silently assumes that the integer values of the corresponding enums are identical. If the enums diverge in the future, this will lead to subtle bugs.

This is also inconsistent with ToUpgradePolicy which uses a safer switch statement. For improved safety and consistency, consider using switch statements for all enum conversions. This will allow the compiler to warn about unhandled cases if the enums change.

Example for ToSchema:

databento::Schema ToSchema(db_c_schema_t schema) {
  switch (schema) {
    case DB_C_SCHEMA_MBO:
      return databento::Schema::Mbo;
    case DB_C_SCHEMA_MBP_1:
      return databento::Schema::Mbp1;
    // ... other cases
    default:
      throw std::invalid_argument("Unknown db_c_schema_t value");
  }
}

If a full switch is too verbose, you could add static_asserts to validate that the enum values match, which would provide compile-time safety.

Comment on lines +65 to +75
databento::VersionUpgradePolicy ToUpgradePolicy(db_c_upgrade_policy_t policy) {
switch (policy) {
case DB_C_UPGRADE_AS_IS:
return databento::VersionUpgradePolicy::AsIs;
case DB_C_UPGRADE_TO_V2:
return databento::VersionUpgradePolicy::UpgradeToV2;
case DB_C_UPGRADE_TO_V3:
default:
return databento::VersionUpgradePolicy::UpgradeToV3;
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The switch statement in ToUpgradePolicy has a default case that falls back to UpgradeToV3. This can hide potential bugs. If a new value is added to db_c_upgrade_policy_t but the switch is not updated, it will silently be treated as UpgradeToV3 instead of causing a compile-time or runtime error.

To make this more robust, it's better to handle each enum member explicitly and have the default case throw an exception for unknown values. This ensures that any future changes to the enum must be handled here.

databento::VersionUpgradePolicy ToUpgradePolicy(db_c_upgrade_policy_t policy) {
  switch (policy) {
    case DB_C_UPGRADE_AS_IS:
      return databento::VersionUpgradePolicy::AsIs;
    case DB_C_UPGRADE_TO_V2:
      return databento::VersionUpgradePolicy::UpgradeToV2;
    case DB_C_UPGRADE_TO_V3:
      return databento::VersionUpgradePolicy::UpgradeToV3;
    default:
      throw std::invalid_argument("Unknown db_c_upgrade_policy_t value");
  }
}

Comment on lines +311 to +328
db_c_live_threaded_t* db_c_live_builder_build_threaded(db_c_live_builder_t* builder) {
if (!builder) {
SetError("builder pointer is null");
return nullptr;
}
try {
ClearError();
auto native_client = builder->builder.BuildThreaded();
auto wrapper = std::make_unique<db_c_live_threaded>();
wrapper->client = std::make_unique<databento::LiveThreaded>(std::move(native_client));
return wrapper.release();
} catch (const std::exception& ex) {
SetError(ex.what());
} catch (...) {
SetError("unexpected error in db_c_live_builder_build_threaded");
}
return nullptr;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The implementation of db_c_live_builder_build_threaded creates a LiveThreaded object on the stack, then move-constructs another one inside a std::unique_ptr. This can be simplified by constructing the LiveThreaded object directly in the std::unique_ptr from the temporary returned by builder->builder.BuildThreaded(). This avoids an extra named variable and an extra move operation, making the code more direct.

db_c_live_threaded_t* db_c_live_builder_build_threaded(
    db_c_live_builder_t* builder) {
  if (!builder) {
    SetError("builder pointer is null");
    return nullptr;
  }
  try {
    ClearError();
    auto wrapper = std::make_unique<db_c_live_threaded>();
    wrapper->client = std::make_unique<databento::LiveThreaded>(
        builder->builder.BuildThreaded());
    return wrapper.release();
  } catch (const std::exception& ex) {
    SetError(ex.what());
  } catch (...) {
    SetError("unexpected error in db_c_live_builder_build_threaded");
  }
  return nullptr;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants